Pages

M,N,K-Game

/********************************/
/* 0. CONTENTS AND INTRODUCTION */
/********************************/

/*  0. this
 *  1. Utilities
 *  2. Tokens
 *  3. Grids
 *  4. Players
 *  5. Connected Tokens Counter/Tester
 *  6. Games
 *  7. Let's play!
 *
 *  This is the source code for programming assignment #16, the last (but not least!)
 *  programming assignment of the 2017-2018 school year. As Knuth wrote in the preface
 *  to Selected Papers on Fun & Games (2011):
 *
 *     “All work and no play makes jack.” ... Thank goodness for mathematics
 *     and computer science, whose methods offer many ways by which our left
 *     brains and our right brains can both be happy, simultaneously.
 *
 *  So let's have some fun! First, I'd like to introduce you to a few friends who always
 *  hang out in the library.
 */
import java.util.Collections;
import java.util.Collection;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.Stack;
import java.util.List;

/****************/
/* 1. UTILITIES */
/****************/

// A couple of tools will be useful.

class Error
{
    public static boolean print(String message)
    {
        System.out.println("Error: " + message + ".");
        return false;
    }
}

class Tokens
{
    public static final Token NO_TOKEN = new ElementaryToken('-');
    public static boolean isNoToken(Token token) { return token == Tokens.NO_TOKEN; }
}

/*************/
/* 2. TOKENS */
/*************/

// Tokens keep track of who's where, like the Xs and Os in a game of Tic-Tac-Toe
// or the black and white stones in a game of Go.  

interface Token
{
    Character symbol();
}

interface PlaceableToken extends Token
{
    int index();
    int row();
    int column();
    boolean putAt(int i);
    boolean belongsOn(Grid grid);
    boolean isOn(Grid grid);
}

interface ErasableToken extends PlaceableToken
{
    boolean erase();
}

interface NonErasableMoveableToken extends PlaceableToken
{
    boolean moveTo(int i);
}

interface VersatileToken extends ErasableToken, NonErasableMoveableToken 
{
}

/** The essence of a token.
 */
class ElementaryToken implements Token
{
    private final Character symbol;
    
    public ElementaryToken(Character c)
    {
        this.symbol = c;
    }
    public final Character symbol()
    {
        return symbol;
    }
    public String toString()
    {
        return symbol.toString();
    }
}

/** A token that is married to a single grid and cannot be placed elsewhere.
 */
class MonogamousPlaceableToken extends ElementaryToken implements PlaceableToken
{
    protected final TokenGrid grid;
    protected int index;

    public MonogamousPlaceableToken(Character symbol, TokenGrid grid)
    {
        super(symbol);
        this.grid = grid;
        this.index = Grid.OFF_GRID;
    }
    public final int index()
    {
        return this.index;
    }
    public final int row()
    {
        return this.index / grid.columnCount();
    }
    public final int column()
    {
        return this.index % grid.columnCount();
    }
    public final boolean isPlaced()
    {
        return this.grid.isGridIndex(this.index);
    }
    public final boolean isOn(Grid grid)
    {
        return grid != null && this.grid == grid && this.isPlaced();
    }
    public final boolean belongsOn(Grid grid)
    {
        return grid != null && this.grid == grid;
    }
    public final boolean putAt(int i)
    {
        if (this.isPlaced())
            return Error.print("Token is in place at " + this.index);

        if (!grid.isGridIndex(i))
            return Error.print("Invalid index: " + i);

        if (!Tokens.isNoToken(grid.get(i)))
            return Error.print("Another token is at " + i);

        this.index = i;
        if (!grid.place(this))
            this.index = Grid.OFF_GRID;
        return this.index != Grid.OFF_GRID;
    }
}

/** A monogamous token that can be placed, erased, or moved.
 */
final class MonogamousVersatileToken extends MonogamousPlaceableToken 
 implements VersatileToken
{
    public MonogamousVersatileToken(Character symbol, TokenGrid grid)
    {
        super(symbol, grid);
    }
    public final boolean erase()
    {
        if (!this.isPlaced())
            return Error.print("Token has not been placed");

        final int eraseFrom = this.index;
        this.index = Grid.OFF_GRID;
        return this.grid.erase(eraseFrom, this);
    }
    public final boolean moveTo(int i)
    {
        if (this.isPlaced() && !this.erase())
            return Error.print("Placed token not erased");

        return putAt(i);
    }
}

/************/
/* 3. GRIDS */
/************/

// Our game boards are grids, and these grids are our boards. Grids are
// where the tokens go. (Are you bored yet?)

interface Grid
{
    final int OFF_GRID = -1;
    int size();
    int rowCount();
    int columnCount();
    boolean isGridIndex(int i);
    int toIndex(int r, int c);
}

interface ImmutableTokenGrid extends Grid
{
    Token get(int i);
}

interface MutableTokenGrid extends ImmutableTokenGrid
{
    boolean place(PlaceableToken token);
    boolean erase(int i, ErasableToken token);
}

/** Like the name suggests: a fixed-size rectangular grid.
 */
class FixedSizeRectangularGrid implements Grid
{
    private final int rowCount, columnCount, size;
    
    protected FixedSizeRectangularGrid(int rowCount, int columnCount)
    {
        this.rowCount = rowCount;
        this.columnCount = columnCount;
        this.size = rowCount * columnCount;
    }
    public final int size()
    {
        return size;
    }
    public final int rowCount()
    {
        return rowCount;
    }
    public final int columnCount()
    {
        return columnCount;
    }
    public final boolean isGridIndex(int i)
    {
        return i >= 0 && i < size;
    }
    public final int toIndex(int r, int c)
    {
        final int i = r * columnCount() + c;
        return isGridIndex(i) ? i : Grid.OFF_GRID;
    }
}

/** A fixed-size rectangular grid that may contain tokens.
 */
final class TokenGrid extends FixedSizeRectangularGrid
 implements MutableTokenGrid
{
    private final char FIRST_COL_HEADER = 'A';
    private final int FIRST_ROW_NUMBER = 1;
    private final Token[] tokens;
    private final String heading;

    public TokenGrid(int rowCount, int columnCount, String heading)
    {
        super(rowCount, columnCount);
        this.heading = heading;
        final int n = this.size();
        tokens = new Token[n];
        for (int i = 0; i < n; i++)
            tokens[i] = Tokens.NO_TOKEN;
    }
    public final Token get(int i)
    {
        return tokens[i];
    }
    public final boolean place(PlaceableToken token)
    {
        if (Tokens.isNoToken(token))
            return Error.print("Attempt to place no token");

        if (!token.isOn(this))
            return Error.print("Token has not been placed on grid");

        final int i = token.index();

        if (!Tokens.isNoToken(this.get(i)))
            return Error.print("A token is already at " + i);

        tokens[i] = token;
        return true;
    }
    public final boolean erase(int i, ErasableToken token)
    {
        if (Tokens.isNoToken(token))
            return Error.print("Attempt to erase no token");

        if (token.isOn(this))
            return Error.print("Token has not been erased");

        if (this.get(i) != token)
            return Error.print("Token found does not match");

        tokens[i] = Tokens.NO_TOKEN;
        return true;
    }

    public String toString()
    {
        StringBuilder sb = new StringBuilder();
        sb.append(this.heading + "\n\n   ");
        Character ch = FIRST_COL_HEADER;
        for (int col = 0; col < this.columnCount(); col++) {
            sb.append(ch.toString() + ' ');
            ch = (char)(ch + 1);
        }
        int i = 0;
        for (Token token : tokens) {
            if (0 == i++ % this.columnCount()) {
                sb.append("\n");
                int rowNum = 1 + (i / this.columnCount());
                sb.append(rowNum);
                sb.append(rowNum < 10 ? "  " : " ");
            }
            sb.append(token.toString() + ' ');
        }
        sb.append("\n");
        return sb.toString();
    }
}

// All aboard! Next stop: PLAYERS ...

/**************/
/* 4. PLAYERS */
/**************/

// Players move their tokens around. Some players make their own tokens too!

interface Player
{
    boolean placeTokenAt(int i);
    boolean eraseTokenAt(int i);
    boolean moveToken(int from, int to);
}

/** Makes a specified number of its own tokens.
 */
class VersatileTokenMaker
{
    private final Character symbol;
    protected final Stack<VersatileToken> tokenStack = new Stack<VersatileToken>();

    public VersatileTokenMaker(int n, Character symbol, TokenGrid grid)
    {
        this.symbol = symbol;
        while (n-- > 0)
            tokenStack.push(new MonogamousVersatileToken(symbol, grid));
    }
    public String toString()
    {
        StringBuilder sb = new StringBuilder();
        sb.append(this.symbol);
        return sb.toString();
    }
}

/** Makes own tokens and can put, erase, or move them on a particular grid.
 */
class VersatileTokenMakerMover extends VersatileTokenMaker implements Player
{
    private final ImmutableTokenGrid grid;

    public VersatileTokenMakerMover(int n, Character symbol, TokenGrid grid)
    {
        super(n, symbol, grid);
        this.grid = grid;
    }
    public final boolean placeTokenAt(int i)
    {
        if (tokenStack.empty())
            return Error.print("Player out of tokens");

        return tokenStack.pop().putAt(i);
    }
    public final boolean eraseTokenAt(int i)
    {
        if (!grid.isGridIndex(i))
            return Error.print("Invalid index " + i);

        Token token = grid.get(i);

        if (Tokens.isNoToken(token))
            return Error.print("No token to erase");

        if (!((ErasableToken)token).erase())
            return Error.print("Failed to erase");

        tokenStack.push((VersatileToken)token);
        return true;
    }
    public final boolean moveToken(int from, int to)
    {
        if (!grid.isGridIndex(from))
            return Error.print("Invalid index: " + from);

        if (!grid.isGridIndex(to))
            return Error.print("Invalid index: " + to);

        Token token = grid.get(from);

        if (Tokens.isNoToken(token))
            return Error.print("No token to move");

        return ((VersatileToken)token).moveTo(to);
    }
}

/**************************************/
/* 5. CONNECTED TOKENS COUNTER/TESTER */
/**************************************/

// Here's a long row to hoe: telling when K tokens are aligned in a row.

interface ElementTester
{
    boolean isOneOfKInARow(int i);
}

final class MatchingTokenCounterTester implements ElementTester
{
    private final TokenGrid grid;
    private final int M, N, K, upper, lower = 0;
    private final Character VOID = Tokens.NO_TOKEN.symbol();

    public MatchingTokenCounterTester(int k, TokenGrid grid)
    {
        this.grid = grid;
        upper = grid.size();
        M = grid.columnCount();
        N = grid.rowCount();
        K = k;
    }

    private int row(int i) { return i / M; }
    private int col(int i) { return i % M; }
    private Character symbol(int i) { return grid.get(i).symbol(); }

    /** Indicates whether the token at index i is one of K matching tokens in a row.
     *
     *  Considers a horizontal, a vertical, and two diagonal rows.
     */
    public boolean isOneOfKInARow(int i)
    {
        return EW(i) >= K || NS(i) >= K || NWSE(i) >= K || NESW(i) >= K;
    }

    // NEXT: The tricky counting methods...

    /** Counts matching tokens connected in a horizontal (east-west) row.
     */
    public int EW(int index)
    {
        final int i = index, row = row(index);
        final Character si = symbol(i);
        if (si == VOID ) return 0;

        int d, j, count = 1;

        for (d = 1, j = i - d;
             j >= lower && count <= K && si == symbol(j) && row(j) == row;
             d++, j = i - d, count++);

        for (d = 1, j = i + d;
             j < upper && count <= K && si == symbol(j) && row(j) == row;
             d++, j = i + d, count++);

        return count;
    }

    /** Counts matching tokens connected in a vertical (north-south) row.
     */
    public int NS(int index)
    {
        final int i = index, col = col(i);
        final Character si = symbol(i);
        if (si == VOID ) return 0;

        int d, j, count = 1;

        for (d = 1, j = i - (d * M);
             j >= lower && count <= K && si == symbol(j) && col(j) == col;
             d++, j = i - (d * M), count++);

        for (d = 1, j = i + (d * M);
             j < upper && count <= K && si == symbol(j) && col(j) == col;
             d++, j = i + (d * M), count++);

        return count;
    }

    /** Counts matching tokens connected in a diagonal (NW-SE) row.
     */
    public int NWSE(int index)
    {
        final int i = index, row = row(i), col = col(i);
        final Character si = symbol(i);
        if (si == VOID ) return 0;

        int d, j, count = 1;

        for (d = 1, j = i - (d * M) - d;
             j >= lower && count <= K && si == symbol(j) &&
                 row(j) == row - d && col(j) == col - d;
             d++, j = i - (d * M) - d, count++);

        for (d = 1, j = i + (d * M) + d;
             j < upper && count <= K && si == symbol(j) &&
                 row(j) == row + d  && col(j) == col + d;
             d++, j = i + (d * M) + d, count++);

        return count;
    }

    /** Counts matching tokens connected in a diagonal (NE-SW) row.
     */
    public int NESW(int index)
    {
        final int i = index, row = row(i), col = col(i);
        final Character si = symbol(i);
        if (si == VOID ) return 0;

        int d, j, count = 1;

        for (d = 1, j = i - (d * M) + d;
             j >= lower && count <= K && si == symbol(j) &&
                 row(j) == row - d  && col(j) == col + d;
             d++, j = i - (d * M) + d, count++);

        for (d = 1, j = i + (d * M) - d;
             j < upper && count <= K && si == symbol(j) &&
                 row(j) == row + d && col(j) == col - d;
             d++, j = i + (d * M) - d, count++);

        return count;
    }
}

/************/
/* 6. GAMES */
/************/

// It's all Fun & Games...

interface Game
{
    public void play();
}

// ... of a certain sort: https://en.wikipedia.org/wiki/M,n,k-game

/* P,q,s,t-M,n,k-Games?
 *
 *  Don't be afraid to think outside the M,n,k-box. Erasing yields Go. What about P
 *  Players? (Why only two?) Or a Quota Q of moves? Maybe the first player should
 *  Start with S tokens to be more fair? What if each player had a Total of T tokens
 *  to put? When they're all in play, they'd have to move. Use your imagination!
 *
 *  See: https://en.wikipedia.org/wiki/Games_played_with_Go_equipment
 *
 */

/** Tic-Tac-Toe
 *
 *  Three in a row.
 *
 *  See also: https://www.webofstories.com/play/donald.knuth/22
 */
final class TicTacToeGame implements Game
{
    private final int N = 3, M = 3, K = 3;
    private final int MAX_TURNS = N * M;
    private final TokenGrid grid = new TokenGrid(N, M, "Tic-Tac-Toe");
    private final ElementTester tester = new MatchingTokenCounterTester(K, grid);

    private final int X_TOK_CNT = (MAX_TURNS / 2) + (MAX_TURNS % 2);
    private final int O_TOK_CNT = MAX_TURNS / 2;
    private final Player playerX = new VersatileTokenMakerMover(X_TOK_CNT, 'X', grid);
    private final Player playerO = new VersatileTokenMakerMover(O_TOK_CNT, 'O', grid);
    private Player currentPlayer = playerX;

    private final List<Integer> plannedMoves = new ArrayList<Integer>(MAX_TURNS);
    private final List<Integer> actualMoves = new ArrayList<Integer>(MAX_TURNS);

    /** Prepare to play a game consisting of the moves provided.
     */
    public TicTacToeGame(List<Integer> moves)
    {
        for (Integer i : moves)
        {
            plannedMoves.add(i);

            if (plannedMoves.size() == MAX_TURNS)
                break;
        }
    }

    /** Prepare to play a complete game consisting of pseudo-random moves.
     */
    public TicTacToeGame()
    {
        for (int i = 0; i < MAX_TURNS; i++)
            plannedMoves.add(i);

        Collections.shuffle(plannedMoves);
    }

    public final void play()
    {
        int turnsTaken = 0;
        for (Integer i : plannedMoves)
        {
            if (!currentPlayer.placeTokenAt(i))
            {
                output(currentPlayer.toString() + " tried to make a bad move.");
                return;
            }
            actualMoves.add(i);
            if (tester.isOneOfKInARow(i))
            {
                output(currentPlayer.toString() + " won!");
                return;
            }
            currentPlayer = currentPlayer == playerX ? playerO : playerX;
            turnsTaken++;
        }
        output(turnsTaken == MAX_TURNS ? "Cats game." : "Game in progress.");
    }

    private void output(String message)
    {
        System.out.println(grid);
        System.out.println(message);
        System.out.print("Moves made: ");
        System.out.println(actualMoves);
    }
}

/** Connect-4
 *
 *  Left as an exercise: Four in a row.
 *
 *  (Don't defy gravity!)
 *
 *  See: https://en.wikipedia.org/wiki/Connect_Four
 */
final class ConnectFourGame implements Game
{
    /* The parameter named moves in the constructor below
     * should contain up to N x M integers (e.g. 6 x 7 = 42)
     * each of which should be greater than or equal to zero
     * and less than M. Duplicates are expected. The numbers
     * identify columns into which tokens are placed.
     */
    public ConnectFourGame(List<Integer> moves)
    {
    }
    public ConnectFourGame()
    {
    }
    public void play()
    {
    }
}

/******************/
/* 7. LET'S PLAY! */
/******************/

// Got game? Let's play!

public class PlayGame
{
    public static void main(String[] args)
    {
        final String TIC_TAC_TOE = "Tic-Tac-Toe";
        final String CONNECT_FOUR = "Connect-4";
        final int n = args.length;

        if (n == 0 || (args[0].equals(TIC_TAC_TOE) && n == 1))
        {
            (new TicTacToeGame()).play();
            return;
        }
        else if (args[0].equals(CONNECT_FOUR) && n == 1)
        {
            (new ConnectFourGame()).play();
            return;
        }
        else if (n > 1)
        {
            final List<Integer> moves = new ArrayList<Integer>();

            for (int i = 1; i < n; i++)
                moves.add(Integer.parseInt(args[i]));

            if (args[0].equals(TIC_TAC_TOE))
            {
                (new TicTacToeGame(moves)).play();
                return;
            }
            if (args[0].equals(CONNECT_FOUR))
            {
                (new ConnectFourGame(moves)).play();
                return;
            }
        }
        System.out.println("Sorry, I don't know how to play " + args[0] + ".");
    }
}