This project is about building a bot for connect 4 game using Java, and it follows the requirements and restrictions described by a platform called riddles.io.
The first approach to this project was using a char[][]
which represents the game board. It would probably be the simplest solution when most people first think of the solution. The bot uses depth-first search algorithm to calculate what would happen at the end of each move, and makes the best move.
An update on the board is given by a String
which represents the whole board. Rather than reading the whole String
to see which position has been updated, I used an int[]
to store current levels of each column in the board. By only storing potential positions to be updated, the handler can only check specific characters in the String
. After updating the board, the bot uses depth-first search and finds the next move it should make.
This approach simply works when there are enough resources to handle this algorithm. In reality, this approach is very costly because everytime we recursively call the dfs method, cases we have to check multiplies. In early searches, it mostly multiplies by 7, and it also redundantly checks cases which have been searched before.
After researching online, I found some good ways to improve the performance. I found out bit manipulation could optimize the algorithm, and using a BitSet
to store the board information was an option. By manipulating bits, it could check vertical, horizontal, and 2 diagonal moves more efficiently everytime we calculate how good a move is. But, I realized it would be more efficient to use a long
to keep track of the board. It is more lightweight and quicker since it is a primitive data type. The board shape of connect 4 game will unlikely to be changed in near future, and long
, which has 64 bits, is good enough to store 42 positions(6*7) in the board. Each bit in the long
represents a specific position in the board and it can assess the information we need by manipulating bits.
To avoid checking cases redundantly, I utilized dynamic programming by implementing a transposition table to memorize cases that have been searched before. So when a case has been searched before, we can just use the value that has been already calculated. In addition to the transposition table, I used alpha-beta pruning algorithm which is commonly used for two-player games.
Despite efforts to improve the performance, it still did not satisfied the time limit specified by the platform. It had exponentially more cases to check in ealier than later, so I came up with a simple trick.
When you start connect 4 game, it is always better to place your discs in the middle because it gives us the most opportunities. Actually, if you start the game with the middle column and play perfectly, you can always win. So, even though the computer may search all moves, it will end up with choosing the middle column for the first move. And when the computer finds out the best move, it means it searched all 4,531,985,219,092 possible positions when the first request is made. If we just hard code the first move, then we can decrease the number of positions to be explored to 1/7 of that massive number. Actually, optimal moves in early connect 4 game are quite simple, and we can help the bot dramatically by providing early moves.
We can dramatically decrease the number of moves which need to be searched, and it can dramatically increase the space for storing moves after first. So it needs some balance between space vs. time and optimization of hard-coded early moves. Also, when we use int
which has 32 bits, it can only safely store 9 moves assuming we store our moves and enemy moves(Integer.MAX_VALUE starts with 2, and it has a high chance of making it negative when we update 10th move). Rather than storing both sides' moves, we can only store the enemy's move because we will always choose the same move given an enemy move. More specifically, we need to consider maximum 7 moves by the emeny, but when once the enemy makes a move, our move is always determined. So we can assume that the next move made by enemy is always based on the specifically determined move we previously made. But, One confusion can occur. If we only keep track of the enemy move, how do we know if we made a move before sequence "3"? We start with column 3, and it could be either "we made the first move, and the enemy made a move which is 3" or "the game is just started and the enemy made a move which is 3". To avoid this confusion, I just put a number indicating whether the bot started first. Then how do we differentiate it from column? It's easy! I used number 8 and 9 which cannot be a column given 7 column board.
Overall, it was a fun challenge. I could get many good resources online to optimize the bot, and it was very nice to learn and build. Balancing between optimization and scalability, I had to time methods and implementations. I would say there are still more ways to optimize my implementation, and it was fun to apply new algorithms. There are more topics and decisions that I did not specifically share here, and I am willing to share if anyone wants to talk with me. Please let me know if you want to talk.