You will learn how to extend the previous recipes with an advanced example. In this case, you will learn how to model a checkers (draughts) board and its pieces in order to comply with the necessary functions to be used with our board-AI framework.
This approach uses a chess board (8 x 8) and its respective number of pieces (12). However, it can be easily parameterized in order to change these values in case we want to have a differently sized board.
First, we need to create a new type of movement for this particular case called MoveDraughts
:
using UnityEngine; using System.Collections; public class MoveDraughts : Move { public PieceDraughts piece; public int x; public int y; public bool success; public int removeX; public int removeY; }
This data structure stores the piece to be moved, the new x
and y
coordinates if the movement is a successful capture, and the position of the piece to be removed.
We will implement two core classes for modeling the pieces and the board, respectively. This is a long process, so read each step carefully:
PieceDraughts.cs
and add the following statements:using UnityEngine; using System.Collections; using System.Collections.Generic;
PieceColor
data type:public enum PieceColor { WHITE, BLACK };
PieceType
data enum:public enum PieceType { MAN, KING };
PieceDraughts
class:public class PieceDraughts : MonoBehaviour { public int x; public int y; public PieceColor color; public PieceType type; // next steps here }
public void Setup(int x, int y, PieceColor color, PieceType type = PieceType.MAN) { this.x = x; this.y = y; this.color = color; this.type = type; }
public void Move (MoveDraughts move, ref PieceDraughts [,] board) { board[move.y, move.x] = this; board[y, x] = null; x = move.x; y = move.y; // next steps here }
if (move.success) { Destroy(board[move.removeY, move.removeX]); board[move.removeY, move.removeX] = null; }
King
:if (type == PieceType.KING) return;
Man
and it reaches the opposite border:int rows = board.GetLength(0); if (color == PieceColor.WHITE && y == rows) type = PieceType.KING; if (color == PieceColor.BLACK && y == 0) type = PieceType.KING;
private bool IsMoveInBounds(int x, int y, ref PieceDraughts[,] board) { int rows = board.GetLength(0); int cols = board.GetLength(1); if (x < 0 || x >= cols || y < 0 || y >= rows) return false; return true; }
public Move[] GetMoves(ref PieceDraughts[,] board) { List<Move> moves = new List<Move>(); if (type == PieceType.KING) moves = GetMovesKing(ref board); else moves = GetMovesMan(ref board); return moves.ToArray(); }
Man
:private List<Move> GetMovesMan(ref PieceDraughts[,] board) { // next steps here }
List<Move> moves = new List<Move>(2);
int[] moveX = new int[] { -1, 1 };
int moveY = 1; if (color == PieceColor.BLACK) moveY = -1;
foreach (int mX in moveX) { // next steps } return moves;
int nextX = x + mX; int nextY = y + moveY;
if (!IsMoveInBounds(nextX, y, ref board)) continue;
PieceDraughts p = board[moveY, nextX]; if (p != null && p.color == color) continue;
MoveDraughts m = new MoveDraughts(); m.piece = this;
if (p == null) { m.x = nextX; m.y = nextY; }
else { int hopX = nextX + mX; int hopY = nextY + moveY; if (!IsMoveInBounds(hopX, hopY, ref board)) continue; if (board[hopY, hopX] != null) continue; m.y = hopX; m.x = hopY; m.success = true; m.removeX = nextX; m.removeY = nextY; }
moves.Add(m);
King
:private List<Move> GetMovesKing(ref PieceDraughts[,] board) { // next steps here }
List<Move> moves = new List<Move>();
int[] moveX = new int[] { -1, 1 }; int[] moveY = new int[] { -1, 1 };
foreach (int mY in moveY) { foreach (int mX in moveX) { // next steps here } } return moves;
int nowX = x + mX; int nowY = y + mY;
while (IsMoveInBounds(nowX, nowY, ref board)) { // next steps here }
PieceDraughts p = board[nowY, nowX];
if (p != null && p.color == color) break;
MoveDraughts m = new MoveDraughts(); m.piece = this;
if (p == null) { m.x = nowX; m.y = nowY; }
else { int hopX = nowX + mX; int hopY = nowY + mY; if (!IsMoveInBounds(hopX, hopY, ref board)) break; m.success = true; m.x = hopX; m.y = hopY; m.removeX = nowX; m.removeY = nowY; }
moves.Add(m); nowX += mX; nowY += mY;
BoardDraughts
in a new file:using UnityEngine; using System.Collections; using System.Collections.Generic; public class BoardDraughts : Board { public int size = 8; public int numPieces = 12; public GameObject prefab; protected PieceDraughts[,] board; }
Awake
function:void Awake() { board = new PieceDraughts[size, size]; }
Start
function. It is important to note that this may vary depending on your game's spatial representation:void Start() { // TODO // initialization and board set up // your implementation may vary // next steps here }
PieceDraught
script:PieceDraughts pd = prefab.GetComponent<PieceDraughts>(); if (pd == null) { Debug.LogError("No PieceDraught component detected"); return; }
int i; int j;
int piecesLeft = numPieces; for (i = 0; i < size; i++) { if (piecesLeft == 0) break; int init = 0; if (i % 2 != 0) init = 1; for (j = init; j < size; j+=2) { if (piecesLeft == 0) break; PlacePiece(j, i); piecesLeft--; } }
piecesLeft = numPieces; for (i = size - 1; i >= 0; i--) { if (piecesLeft == 0) break; int init = 0; if (i % 2 != 0) init = 1; for (j = init; j < size; j+=2) { if (piecesLeft == 0) break; PlacePiece(j, i); piecesLeft--; } }
private void PlacePiece(int x, int y) { // TODO // your own transformations // according to space placements Vector3 pos = new Vector3(); pos.x = (float)x; pos.y = -(float)y; GameObject go = GameObject.Instantiate(prefab); go.transform.position = pos; PieceDraughts p = go.GetComponent<PieceDraughts>(); p.Setup(x, y, color); board[y, x] = p; }
Evaluate
function with no parameters:public override float Evaluate() { PieceColor color = PieceColor.WHITE; if (player == 1) color = PieceColor.BLACK; return Evaluate(color); }
Evaluate
function with a parameter:public override float Evaluate(int player) { PieceColor color = PieceColor.WHITE; if (player == 1) color = PieceColor.BLACK; return Evaluate(color); }
private float Evaluate(PieceColor color) { // next steps here }
float eval = 1f; float pointSimple = 1f; float pointSuccess = 5f;
int rows = board.GetLength(0); int cols = board.GetLength(1);
int i; int j;
for (i = 0; i < rows; i++) { for (j = 0; j < cols; j++) { PieceDraughts p = board[i, j]; if (p == null) continue; if (p.color != color) continue; Move[] moves = p.GetMoves(ref board); foreach (Move mv in moves) { MoveDraughts m = (MoveDraughts)mv; if (m.success) eval += pointSuccess; else eval += pointSimple; } } }
return eval;
public override Move[] GetMoves() { // next steps here }
List<Move> moves = new List<Move>(); int rows = board.GetLength(0); int cols = board.GetLength(1); int i; int j;
for (i = 0; i < rows; i++) { for (j = 0; i < cols; j++) { PieceDraughts p = board[i, j]; if (p == null) continue; moves.AddRange(p.GetMoves(ref board)); } }
return moves.ToArray();
The board works in a similar fashion to the previous board, but it has a more complex process due to the rules of the game. The movements are tied to the pieces' moves, thus creating a cascading effect that must be handled carefully. Each piece has two types of movement, depending on its color and type.
As we can see, the high-level rules are the same. It just requires a little bit of patience and thinking in order to develop good evaluation functions and procedures for retrieving the board's available moves.
The Evaluate
function is far from being perfect. We implemented a heuristic based solely on the number of available moves and captured opponent pieces, giving room for improvement in order to avoid movements where a player's piece could be captured in the rival's next move.
Also, we should make our own changes to the PlacePiece
function in the BoardDraughts
class. We implemented a direct method that probably doesn't fit your game's spatial setup.