Showing posts with label AI. Show all posts
Showing posts with label AI. Show all posts

Tuesday, May 23, 2023

Let there be Life

 When I said I was going to program the game from scratch, I wasn’t kidding. As I am trying to move back to test driven development, as I believe the AI world of programming will become more natural language programming with the responsibility of the programmers to make sure that what the natural language produces actually works properly so creating and testing programs will become a vital skill for the future of programming. I don’t think programmers will be going away, but their roles will be more focused on design, testing, security, and optimization. Still, using a testing framework would be third party, so I wrote my own simple testing framework (when I post the final version the tests will be included).

Writing automated tests seems to be slow, but I notice that I find bugs much quicker and am a bit more confident that my code does what it is supposed to. My tests are not as thorough as they should be but good enough for the scope of the project I am working on.

When creating the game, I realized my idea of moving cells was very problematic, so opted for a growing effect with the eating of cells being a predominant feature. Text versions seemed to work so I bit the bullet and created a graphical version (manually tested as graphics/GUIs are way too painful to automate, and if the underlying code it calls is tested there shouldn’t be a problem).

The simulation was automated and just generating random grids results in really cool results. The first phase has been posted so Thursday I will be focusing on creating an editor and the weekend on putting together a short campaign where you will be given a map and need to place your cells in such a way that they will take over the map. Something has come up for Wednesday, so while I might get an hour or two in, it is likely not going to be very much. Hopefully I will have something Thursday night to show, as Friday is busy. Why everything comes up when I want to do a Game Jam is annoying, but it is what it is.

The first build is available at https://spelchan.com/games/y2023/HexLife/index.php

See you Thursday???

Monday, March 14, 2022

Making Santa-Tac-Toe part 3 of 4

 Part 3: Random Moves Meet Monty Carlo

Thanks in small part to AlphaGo, there is a huge interest in Monty Carlo Tree Search (MCTS). As the name implies, this is built off the idea of a Monty Carlo Simulation, where you run a large number of random simulations to help determine the best configuration of what is being simulated. Tic-Tac-Toe is too simple for MCTS, but the simulation aspect would be simple to implement and could be interesting so lets do that. We will then look at what would need to be done to turn the simulation into a proper MCTS AI. 

The heart of a simulation is the ability to play a random game. This is simply a matter of making random moves until the game is in a win or tie state, at which point we return the state of the game so we can keep track of the wins and losses.

playMockGame(board, curPlayer) {
   
let b = new TTTBoard(board.getTemplate());
    let
state = b.getGameState();
    let
p = curPlayer;
    while
(state.state === TTTConsts.PLAYING) {
        b.
setTileByIndex(super.getNextMove(b, p), p);
       
p = (p === TTTConsts.X) ? TTTConsts.O : TTTConsts.X;
       
state = b.getGameState();
   
}
   
return state;
}


We want to score each potential move that a player can make, so simply loop over all the moves adding them to our scores array which we will be using for the next move method that our API requires we implement.

buildScoreBoard(board, curPlayer) {
   
let scores = [];
    for
(let i = 0; i < 9; ++i)
        scores.
push(this.scoreTile(board, i, curPlayer));
    return
scores;
}

Scoring a tile is simply a matter of making sure the move is valid, followed by playing several mock games and counting how many of the games the AI wins versus the losses.

scoreTile(board, index, curPlayer) {
   
if (board.getTileByIndex(index) !== TTTConsts.EMPTY)
       
return -2 * this.trials;
    let
winState = (curPlayer === TTTConsts.X) ? TTTConsts.X_WON : TTTConsts.O_WON;
    let
loseState = (curPlayer === TTTConsts.X) ? TTTConsts.O_WON : TTTConsts.X_WON;
    let
score = 0;
    this
.testBoard.clone(board);
    this
.testBoard.setTileByIndex(index, curPlayer);
    let
opponent = (curPlayer === TTTConsts.X) ? TTTConsts.O : TTTConsts.X;
    for
(let i = 0; i < this.trials; ++i) {
       
let state = this.playMockGame(this.testBoard, opponent);
        if
(state.state === winState) score += 1;
        if
(state.state === loseState) score -= 1;
   
}
   
return score;
}

Finally, implementing getNextMove is simply a matter of calling buildScoreBoard and then picking the highest score. We add a random value to each score as a tiebreaker.

getNextMove(board, playerToMove= TTTConsts.EMPTY) {
   
let scores = this.buildScoreBoard(board,playerToMove);
    let
index = 0;
    let
high = scores[0] + Math.random();
    for
(let i = 1; i < 9; ++i) {
       
let temp = scores[i] + Math.random();
        if
(temp > high) {
            index = i
;
           
high = temp;
       
}
    }

   
return index;
}

The MCTS algorithm is a bit more involved than what we are doing, but with such a small tree, implementing a full MCTS is purely academic. We would need to track a bit more information for each node in the game to properly do MCTS. Lets quickly take a look at the algorithm. It is broken into 4 basic steps.

1 Selection. The idea here is that we pick a node that we want to explore further. This is usually done by looking at the win/loss ratio in combination with the times the node has been looked at for the nodes that are not fully expanded. We want to pick the most promising node but at the same time want to make sure that each node is explored enough to properly estimate it’s worthiness. There are many methods to do this though the simplest is to use threshold * (wins+1)/(plays+1) but there are better methods available.

2 Expansion is simply to add one of the unexplored nodes of the target onto our tree. Instead of just having a scoreboard for the current round, we would need to track all the nodes we have expanded in some type of tree structure and keep track of the win/loss ratio for all of them.

3 Simulation (playing random game), what we are doing already.

4 backpropagation. Taking the score the expanded node received and passing it through all the parent nodes adjusting their probability. Remember to consider who has won so that you invert the win/loss ratio from opponent nodes.

The big issue here is that it is easy to run out of memory as you expand. One solution to this is to cap the number of nodes allocated and when you run out of nodes go into a pruning stage where you remove nodes of poorly performing choices. A node pool is a good way of doing this as when the pool is empty, you do a pruning pass adding pruned nodes (including children) back into the pool.

Now that we have several AI agents, we can put together our Christmas game. This will be covered in the next part.


Monday, February 14, 2022

Making Santa-Tac-Toe part 2 of 4

 Part 2: Solving Using a Tree

With the basic game class that was created in the last part, we are ready to create a better player to play against. This is where the computer can have an advantage over us humans as they can examine many more moves per second than we can. This allows more exploration of the possible moves, and with a simple game like Tic-Tac-Toe, can even explore all the possible moves when deciding what move they wish to make. How is this possible? When you think about a game, it can be represented as a tree structure as shown in the image below.


The first move has nine options. The second has 8 options for each of the 9 boards. The third move has 7 options for each of the 72 boards and so on. The total number of board configurations is 9!, which is 9 factorial or 9x8x7x6x5x4x3x2x1 for a grand total of 362,880. As each move leads to a set of other moves, the game can be represented as a tree. The AI then just hast to start on the node of the tree representing the current state of the board then find the move most likely to win. There is one problem, namely that the opponent also wants to win.

We need to be able to pick the best move to make for the computer while also assuming that the opponent will always pick the best move that they can. This may sound like a problem, as what happens if the opponent doesn’t pick the best move? The answer is that the AI will still be in a good position, possibly even able to win the game faster by taking advantage of the mistake. Still, if you are using some of the pruning techniques we will be discussing, this could be a problem as the AI may not have stored information about that move and will have to search from scratch.

The basic approach then is to recursively search the tree taking the best move from whoever the current player is.  As the opponent winning is bad for us, we will invert the score from the opponent so will weigh the moves that the opponent wins as an undesirable position for us. This is implemented as a recursive pair of methods.  ScoreTiles simply takes a board and scores each possible move on the board by calling scoreTile. ScoreTile checks to see if a particular tile is a valid move and if so if that move will result in a win. If the move is not an immediate win or tie, we need to look at the next moves in the game taking the highest score from that and inverting it. This recursion will repeat until a winning or tie move is reached.

scoreTiles(board, curPlayer) {
   
let scores = [];

    for
(let i = 0; i < 9; ++i) {
       
if (board.getTileByIndex(i) === TTTConsts.EMPTY) {
            scores.
push(this.scoreTile(board, i, curPlayer));
       
} else
           
scores.push(TTTConsts.INVALID_MOVE);
   
}

   
return scores;
}

scoreTile(board, index, curPlayer) {
   
if (board.getTileByIndex(index) !== TTTConsts.EMPTY) return TTTConsts.INVALID_MOVE;
    this
.testBoard.clone(board);
    this
.testBoard.setTileByIndex(index, curPlayer);
    let
winState = (curPlayer === TTTConsts.X) ? TTTConsts.X_WON : TTTConsts.O_WON;
    let
state = this.testBoard.getGameState();
    if
(state.state === TTTConsts.PLAYING) {
       
// recurse
       
let rboard = new TTTBoard();
       
rboard.clone(this.testBoard);
        let
opponent = (curPlayer === TTTConsts.X) ? TTTConsts.O : TTTConsts.X;
        let
scores= this.scoreTiles(rboard, opponent);
        let
highIndex = 0;
        for
(let i = 1; i < 9; ++i)
       
if (scores[highIndex] < scores[i])
            highIndex = i
;
        return
-scores[highIndex];
   
}
   
return (state.state === winState) ? 1 : 0;
}


The getNextMove API call then simply calls the ScoreTiles for the existing move and picks the move with the highest score.

getNextMove(board, playerToMove = TTTConsts.EMPTY) {
   
let scores = this.scoreTiles(board, playerToMove);
   
console.log(scores)
   
// return best move
   
let highIndex = 0;
    for
(let i = 1; i < 9; ++i)
       
if (scores[highIndex] < scores[i])
            highIndex = i
;
    return
highIndex;
}


The search tree for Tic-Tac-Toe is a small enough tree that we can fully explore it, but what if it is not? More complicated games may not allow for us to search the entire tree in a reasonable timespan. For these games we need to come up with a different way of scoring the state of the board. This will be game specific, but one approach I would consider if there is not a good way of finding a weight for the board would be to recurse to the desired end depth and then simply do a series of random games and return a score based on how many wins occurred. Limiting the depth that you search also is a way to reduce the difficulty of the game.

To speed up searching, there are ways of pruning the search tree. If you assume that your opponent will always take the winning move if it is available, you can stop searching for an opponent move once there is a winning move available. This may not sound like much but can reduce the search by a surprising amount. 

One of the new and highly effective ways of picking moves is the use of the Monty Carlo Tree Search algorithm. We will not be implementing the full algorithm but will do the starting point and explain the algorithm in the next part.