Implementing a checkers rival

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.

Getting ready…

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.

How to do it…

We will implement two core classes for modeling the pieces and the board, respectively. This is a long process, so read each step carefully:

  1. Create a new file called PieceDraughts.cs and add the following statements:
    using UnityEngine;
    using System.Collections;
    using System.Collections.Generic;
  2. Add the PieceColor data type:
    public enum PieceColor
    {
        WHITE,
        BLACK
    };
  3. Add the PieceType data enum:
    public enum PieceType
    {
        MAN,
        KING
    };
  4. Start building the PieceDraughts class:
    public class PieceDraughts : MonoBehaviour
    {
        public int x;
        public int y;
        public PieceColor color;
        public PieceType type;
        // next steps here
    }
  5. Define the function for setting up the piece logically:
    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;
    }
  6. Define the function for moving the piece on the board:
    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
    }
  7. If the move is a capture, remove the corresponding piece:
    if (move.success)
    {
        Destroy(board[move.removeY, move.removeX]);
        board[move.removeY, move.removeX] = null;
    }
  8. Stop the process if the piece is King:
    if (type == PieceType.KING)
        return;
  9. Change the type of piece if it is 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;
  10. Define the function for checking if a move is inside the bounds of the board:
    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;
    }
  11. Define the general function for retrieving the possible moves:
    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();
    }
  12. Start implementing the function for retrieving the moves when the piece's type is Man:
    private List<Move> GetMovesMan(ref PieceDraughts[,] board)
    {
        // next steps here
    }
  13. Add the variable for storing the two possible moves:
    List<Move> moves = new List<Move>(2);
  14. Define the variable for holding the two possible horizontal options:
    int[] moveX = new int[] { -1, 1 };
  15. Define the variable for holding the vertical direction depending on the piece's color:
    int moveY = 1;
    if (color == PieceColor.BLACK)
        moveY = -1;
  16. Implement the loop for iterating through the two possible options and return the available moves. We will implement the body of the loop in the next step:
    foreach (int mX in moveX)
    {
        // next steps
    }
    return moves;
  17. Declare two new variable for computing the next position to be considered:
    int nextX = x + mX;
    int nextY = y + moveY;
  18. Test the possible option if the move is out of bounds:
    if (!IsMoveInBounds(nextX, y, ref board))
        continue;
  19. Continue with the next option if the move is being blocked by a piece of the same color:
    PieceDraughts p = board[moveY, nextX];
    if (p != null && p.color == color)
        continue;
  20. Create a new move to be added to the list because we're good-to-go:
    MoveDraughts m = new MoveDraughts();
    m.piece = this;
  21. Create a simple move if the position is available:
    if (p == null)
    {
        m.x = nextX;
        m.y = nextY;
    }
  22. Otherwise, test whether the piece can be captured and modify the move accordingly:

    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;
    }
  23. Add the move to the list:
    moves.Add(m);
  24. Start to implement the function for retrieving the available moves when the piece's type is King:
    private List<Move> GetMovesKing(ref PieceDraughts[,] board)
    {
        // next steps here
    }
  25. Declare the variable for holding the possible moves:
    List<Move> moves = new List<Move>();
  26. Create the variables for searching in four directions:
    int[] moveX = new int[] { -1, 1 };
    int[] moveY = new int[] { -1, 1 };
  27. Start implementing the loop for checking all the possible moves, and retrieve those moves. The next step will implement the body of the inner loop:
    foreach (int mY in moveY)
    {
        foreach (int mX in moveX)
        {
            // next steps here
        }                   
    }
    return moves;
  28. Create the variables for testing the moves and advances:
    int nowX = x + mX;
    int nowY = y + mY;
  29. Create a loop for going in that direction until the board's bounds are reached:
    while (IsMoveInBounds(nowX, nowY, ref board))
    {
        // next steps here
    }
  30. Get the position's piece reference:
    PieceDraughts p = board[nowY, nowX];
  31. If it is a piece of the same color, go no further:
    if (p != null && p.color == color)
        break;
  32. Define a variable for creating the new available move:
    MoveDraughts m = new MoveDraughts();
    m.piece = this;
  33. Create a simple move if the position is available:
    if (p == null)
    {
        m.x = nowX;
        m.y = nowY;
    }
  34. Otherwise, test whether the piece can be captured and modify the move accordingly:
    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;
    }
  35. Add the move and advance a step towards the current direction:
    moves.Add(m);
    nowX += mX;
    nowY += mY;
  36. Create a new class called 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;
    }
  37. Implement the Awake function:
    void Awake()
    {
        board = new PieceDraughts[size, size];
    }
  38. Start implementing the 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
    }
  39. Throw an error message if the template object doesn't have an attached PieceDraught script:
    PieceDraughts pd = prefab.GetComponent<PieceDraughts>();
    if (pd == null)
    {
        Debug.LogError("No PieceDraught component detected");
        return;
    }
  40. Add iterator variables:
    int i;
    int j;
  41. Implement the loop for placing the white pieces:
    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--;
        }
    }
  42. Implement the loop for placing the black pieces:
    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--;
        }
    }
  43. Implement the function for placing a specific piece. This could change in your game depending on its visualization:
    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;
    }
  44. Implement the Evaluate function with no parameters:
    public override float Evaluate()
    {
        PieceColor color = PieceColor.WHITE;
        if (player == 1)
            color = PieceColor.BLACK;
        return Evaluate(color);
    }
  45. Implement the Evaluate function with a parameter:
    public override float Evaluate(int player)
    {
        PieceColor color = PieceColor.WHITE;
        if (player == 1)
            color = PieceColor.BLACK;
        return Evaluate(color);
    }
  46. Start implementing the general function for evaluation:
    private float Evaluate(PieceColor color)
    {
        // next steps here
    }
  47. Define the variables for holding the evaluation and assigning points:
    float eval = 1f;
    float pointSimple = 1f;
    float pointSuccess = 5f;
  48. Create variables for holding the board's bounds:
    int rows = board.GetLength(0);
    int cols = board.GetLength(1);
  49. Define variables for iteration:
    int i;
    int j;
  50. Iterate throughout the board to look for moves and possible captures:
    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;
            }
        }
    }
  51. Retrieve the evaluation value:
    return eval;
  52. Start developing the function for retrieving the board's available moves:
    public override Move[] GetMoves()
    {
        // next steps here
    }
  53. Define the variables for holding the moves and the board's boundaries, and handling iteration:
    List<Move> moves = new List<Move>();
    int rows = board.GetLength(0);
    int cols = board.GetLength(1);
    int i;
    int j;
  54. Get the moves from all the available pieces on the board:
    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));
        }
    }
  55. Return the moves found:
    return moves.ToArray();

How it works…

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.

There is more…

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.98.208