In order to make use of the previous recipes, we will devise a way to implement a rival for a popular game: tic-tac-toe. Not only does it help us extend the base classes, but it also gives us a way to create rivals for our own board games.
We will need to create a specific move class for the tic-tac-toe board derived from the parent class we created at the beginning of the chapter:
using UnityEngine; using System.Collections; public class MoveTicTac : Move { public int x; public int y; public int player; public MoveTicTac(int x, int y, int player) { this.x = x; this.y = y; this.player = player; } }
We will create a new class, deriving it from Board
, override its parent's methods, and create new ones.
BoardTicTac
class, deriving it from Board
, and add the corresponding member variables for storing the board's values:using UnityEngine; using System; using System.Collections; using System.Collections.Generic; public class BoardTicTac : Board { protected int[,] board; protected const int ROWS = 3; protected const int COLS = 3; }
public BoardTicTac(int player = 1) { this.player = player; board = new int[ROWS, COLS]; board[1,1] = 1; }
private int GetNextPlayer(int p) { if (p == 1) return 2; return 1; }
private float EvaluatePosition(int x, int y, int p) { if (board[y, x] == 0) return 1f; else if (board[y, x] == p) return 2f; return -1f; }
private float EvaluateNeighbours(int x, int y, int p) { float eval = 0f; int i, j; for (i = y - 1; i < y + 2; y++) { if (i < 0 || i >= ROWS) continue; for (j = x - 1; j < x + 2; j++) { if (j < 0 || j >= COLS) continue; if (i == j) continue; eval += EvaluatePosition(j, i, p); } } return eval; }
public BoardTicTac(int[,] board, int player) { this.board = board; this.player = player; }
public override Move[] GetMoves() { List<Move> moves = new List<Move>(); int i; int j; for (i = 0; i < ROWS; i++) { for (j = 0; j < COLS; j++) { if (board[i, j] != 0) continue; MoveTicTac m = new MoveTicTac(j, i, player); moves.Add(m); } } return moves.ToArray(); }
public override Board MakeMove(Move m) { MoveTicTac move = (MoveTicTac)m; int nextPlayer = GetNextPlayer(move.player); int[,] copy = new int[ROWS, COLS]; Array.Copy(board, 0, copy, 0, board.Length); copy[move.y, move.x] = move.player; BoardTicTac b = new BoardTicTac(copy, nextPlayer); return b; }
public override float Evaluate(int player) { float eval = 0f; int i, j; for (i = 0; i < ROWS; i++) { for (j = 0; j < COLS; j++) { eval += EvaluatePosition(j, i, player); eval += EvaluateNeighbours(j, i, player); } } return eval; }
public override float Evaluate() { float eval = 0f; int i, j; for (i = 0; i < ROWS; i++) { for (j = 0; j < COLS; j++) { eval += EvaluatePosition(j, i, player); eval += EvaluateNeighbours(j, i, player); } } return eval; }
We define a new type of move for the board that works well with the base algorithms because they make use of it only at a high level as a data structure. The recipe's bread and butter come from overriding the virtual functions from the Board
class in order to model the problem. We use a two-dimensional integer array for storing the players' moves on the board (0 represents an empty place), and we work out a heuristic for defining the value of a given state regarding its neighbors.
The functions for evaluating a board's (state) score have an admissible heuristic, but it's probably not optimal. It is up to us to revisit this problem and refactor the body of the aforementioned functions in order to have a better tuned rival.