/********************************/
/* 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] + ".");
}
}