Showing posts with label Test Driven Development. Show all posts
Showing posts with label Test Driven Development. Show all posts

Friday, January 14, 2022

Making Santa-Tac-Toe part 1 of 4

 Part 1: Preparing for Santa and Krampus

Santa Tac Toe is a JavaScript port of a game I created when working as a teaching assistant for a course in AI. We had a team project where groups would create AI controlled players for the game of Amazons. Our summer terms are condensed to two months with double the lecture and lab time. To help students start their AI, I wrote a simple Tic Tac Toe game in Java to demonstrate recursive AIs. The advantage here is the search tree is so small that things like alpha-beta pruning and techniques for scoring a position are not covered so they would still need to figure those out themselves but would still have a starting point. This approach will be covered in part 2.  A popular technique for games, largely due to Alpha Go, is Monty Carlo Tree Search. While I didn’t want to do a full MCTS AI, in Part 3 we look at the starting point for this technique. Finally, in Part 4 we will make everything pretty by creating the GUI for the game.

 As most of my personal projects tend to be small, I tend to rely a bit too much on manual testing. This is not a bad thing, but as projects get larger and more people are involved, automated testing makes more sense. One of the trendy test driven development methods is red/green testing, which does sound like a good idea but has its detractors. The idea is you write a test first and after it fails, you write the code to make it pass the test. For code where it is easy to write automated tests, I am using Jasmine to write tests. For user interface and animation related stuff, I am still largely going to rely on manual testing, but as problems here will quickly be apparent this shouldn’t be too bad of a compromise. I do need to find a decent GUI testing suite but am not sure such a beast actually exists. For those of you interested in my test code, it is in the repository.


The board is simply an array of numbers, set up as a single dimensional array stored as shown in the figure above. Numbers were chosen instead of characters due to being faster. An alternative approach would be to store the board as a string, but strings are immutable in JavaScript. Altering a string creates a new one, and if you are replacing the old string then it gets garbage collected. To aid debugging, the constructor takes a string representation of the board and can create a string representation for logging or other debugging tasks.

Setting and getting tiles is trivial enough. The key method here is the getGameState() method which fills out the TTTState class with information about the current state of the board. Simply put, the game can be in progress, the X player (Santa) could have won, the O player (Krampus) could have won, or there are no more moves so the game ends in a tie (Cat). For win conditions, the line that forms the win is necessary so a way of checking lines on the board is needed.

The checkLine method assumes the starting location is correct for the direction being checked as this is meant for internal use. All that needs to be done is to check if three tiles all contain the same non-empty value and return true if they are. Because we are storing the value in an array, we can use steps to find the appropriate locations. Horizontal lines step at +1, vertical lines step at +3, diagonal step at +4 for right diagonals and +2 for left diagonals.

checkLine(x,y,direction) {
   
let index = x + y * 3;
    let
startTile = this.board[index];
    if
(startTile === TTTConsts.EMPTY)
       
return false;
    let
offset = TTTConsts.LINE_OFFSETS[direction];
    return
(this.board[index+offset] === startTile) &&
        (
this.board[index+offset+offset] === startTile);
}


With this we can work out the state of the game. We start by assuming the game is in progress. We then quickly check to see if the board is full. If so, we know there is either a win or a tie, so set the state to tie but continue checking to see if it is actually a win. Each of the possible horizontal and vertical wins are checked using the checkLine method above. Finally we check the two diagonal moves.

getGameState() {We now have the basics for the game but need players. As we are focusing on AI players, we will start with the easiest possible AI player, one that makes moves at random. When called, this agent simply picks a tile at random, then we see if that is a valid tile and if not move to the first open tile.
    this.gameState.state = TTTConsts.PLAYING;
   
// potential tie check     let tie = true;     for (let i = 0; i < 9; ++i)         if (this.board[i] === TTTConsts.EMPTY)             tie = false;
    if
(tie)         this.gameState.state = TTTConsts.TIE;
   
// win check     for(let i = 0; i < 3; ++i) {         if (this.checkLine(i,0,TTTConsts.VERTICAL))             this.gameState.setWinLine(this, i,0,TTTConsts.VERTICAL);
        if
(this.checkLine(0,i,TTTConsts.HORIZONTAL))             this.gameState.setWinLine(this, 0,i,TTTConsts.HORIZONTAL);     }
   
if (this.checkLine(0,0,TTTConsts.RIGHT_DIAGONAL))         this.gameState.setWinLine(this, 0,0,TTTConsts.RIGHT_DIAGONAL);
    if
(this.checkLine(2,0,TTTConsts.LEFT_DIAGONAL))         this.gameState.setWinLine(this, 2,0,TTTConsts.LEFT_DIAGONAL);
    return this
.gameState; }

We now have the basics for the game but need players. As we are focusing on AI players, we will start with the easiest possible AI player, one that makes moves at random. When called, this agent simply picks a tile at random, then we see if that is a valid tile and if not move to the first open tile.

class AIPlayer {
    constructor(playerID) {
        this.playerID = playerID;
    }
   
findNextFreeIndex(board, startIndex = 0) {         let index = -1;         let nextIndex = startIndex         do {             if (board.getTileByIndex(nextIndex) === TTTConsts.EMPTY)                 index = nextIndex;             nextIndex = (nextIndex+1)%9;         } while ((index === -1) && (nextIndex !== startIndex));
        return
index;     }
   
getNextMove(board, playerToMove= TTTConsts.EMPTY) {         return this.findNextFreeIndex(board, Math.floor(Math.random() * 9));     } }

This random player will be expanded upon for our Monty player in part 3, but next we want to look at the traditional recursive AI, which we will do in the next part.


Wednesday, November 8, 2017

Test Driven Disassembly

When I first started programming, the procedure was simple. You would write the program and then you would test the program. The testing was generally manual testing simply making sure that the program would do what you wanted. This is fine for when working on small or personal projects, but when projects get larger this is not a good way of doing things. Changing things could cause code to break but as it is not tested for it can go unnoticed for a long time and when discovered would require a lot of effort to find and fix.

The idea of automated testing helps solve this problem by making the testing process easy as one just needs to run the test after making changes to see if anything is broken. This does require that the tests exist which can be a problem as writing tests after the code has been completed makes writing the test a chore that can be skipped if one is behind schedule. It also has the problem of the test only testing what is already known to work.

Test driven development moves the testing to the top of the development loop. This has the advantage that the tests are written before the code so code always has test code. You then make sure that the tests fail and then write the code and get the code to pass the tests. You also have the advantage of thinking about how exactly you are going to test things and may uncover issues before you have even started writing the code. A comparison of the three methods is shown in the flowcharts below.



As with pretty much every approach to programming, dogmatism can take over and the advantages of test driven development can quickly be replaced by useless burdens. If you find yourself having to write tests for basic getters and setters then you have fallen into the dogmatism rabbit hole. I have been taking a middle ground with my work coming up with tests before writing code. As some things are simply too difficult to write automated tests for, especially non-deterministic programs such as with many games, manual testing is an option as long as you have a clear test plan. Automated testing is a preference as the tests are always ran so problems are detected earlier.

For my disassembler, the test is simply being able to disassemble known code into the proper instructions. My original plan for the assembly code was to write some test assembly language that would cover all the instructions with the various address modes for the instructions. The code wouldn’t have to perform anything useful, just cover the broad range of assembly instructions. This got me thinking about non-standard instructions.

There are future use operation codes (OP codes) that the 6502 has that when used will do things. As this functionality is unofficial, using such instructions is not wise since the presence is not guaranteed, but some programmers would use these instructions if it would save memory or cycles. As I do want my emulator to work with at least some real cartridges, which may use unofficial instructions, I need my disassembler to be able to detect these instructions and alert me to the use of the instructions so I can figure out what the instruction does and implement it in the future.

This means that all 256 possible OP codes need to be tested. As I want to be able to disassemble from any arbitrary point in memory, this simply meant that my test could be procedurally done. My test memory simply filled memory with the numbers from 0 to 255 so if I set the disassembly address in sequence, I would have all the instructions with the subsequent bytes being the address or value to be used. The fact that the instructions were of different lengths was not a big deal as we would be manually controlling the disassembly address. The list of instructions is something that I have so creating the test result list to compare to was very simple.

When running the test, if there is an error, it is still possible that my test list is incorrect, but this is obvious enough to determine. Once the disassembler is in a state that it can disassemble the complete list, it is probably working so as far as tests are concerned, this is a good way of testing. Once I wrote my disassembler, I did have to fix the list but also did find some issues so overall the test did work. Next week I will go into the disassembler which was surprisingly easy to write.