As a casual attempt to accomplish a Grand Assignment, I created a Reversi game with Python. The project is open-source on GitHub and you can view it with the link above.

Screenshot

The game implements the following functionality:

  • Graphical User Interface (GUI), using PyQt5
  • Built-in AI implemented as a heuristic searching (and evaluation) algorithm

The core functionality of the game is pretty simple, and should be easy for anyone familiar with the Reverso game. The code is in reversi.py.

It’s the GUI and the AI part that differentiates implementations of the Reversi game. Since GUI isn’t very hard to create with Qt, I’ll focus on the AI algorithm here.

As a general (and relatively easy) way to create an AI for the game, heuristic searching well balances between coding complexity and the competence of the resulting algorithm. Heuristic searching is basically a complete searching tree at limited depth, equipped with a heuristic evaluation function that determines the situation of the game board.

Heuristic Evaluation

From my previous experiences in playing Reversi with others, I’ve learnt that it’s important to occupy side grids and corner grids, while avoiding the opponent to occupy them. With some research and calculation, this boils down to a predefined weight table:

SCORE = [
    [  500, -150, 30, 10, 10, 30, -150,  500],
    [ -150, -250,  0,  0,  0,  0, -250, -150],
    [   30,    0,  1,  2,  2,  1,    0,   30],
    [   10,    0,  2, 16, 16,  2,    0,   30],
    [   10,    0,  2, 16, 16,  2,    0,   30],
    [   30,    0,  1,  2,  2,  1,    0,   30],
    [ -150, -250,  0,  0,  0,  0, -250, -150],
    [  500, -150, 30, 10, 10, 30, -150,  500]
]

This is a good start but may not be sufficient in complicated scenarios. While it’s good to try to occupy sides and corners, it’d be of little value when they’re easily captured by the opponent.

Given the idea above, I came up with this “chess liberty” solution. The score will be decreased if a chess is “too liberated”.

for dx in [-1, 0, 1]:
    for dy in [-1, 0, 1]:
        tx, ty = x + dx, y + dy
        if 0 <= tx < BS and 0 <= ty < BS and board[tx][ty] == EMPTY:
            liberty += 1
if chess == BLACK:
    c1 += 1
    s1 += SCORE[x][y] - liberty * LIBERTY
else:
    c2 += 1
    s2 += SCORE[x][y] - liberty * LIBERTY

where s1 is the score for the black side, and s2 is the score for the white side.

The final part is the side chess check (code). It checks if a corner may be occupied by the opponent (having own chess around), and if it’s occupied, add additional score if there are more chesses of the same color.

Searching tree

A plain searching algorithm would be really plain. The black side wants the score to be as high as possible, while the white side wants the exact opposite.

A pseudo-code of the algorithm is given below:

MAX = 999999

function heuristicSearch(game, side, depth)
  if depth <= 0
    return null, heuristicScore(game)

  want_max = (side == BLACK)
  bestMove = null
  if want_max
    bestScore = -MAX - 1
  else
    bestScore = +MAX + 1

  for move in game.availableMoves
    game.perform move
    // Recurse
    subMove, subScore = heuristicSearch(game, side, depth - 1)
    game.undo

    if want_max
      if subScore > bestScore
        bestScore = subScore
        bestMove = move
    else
      if subScore < bestScore
        bestScore = subScore
        bestMove = move
  return bestMove, bestScore

That’s the abstract of the searching procedure. There are a few things to do before it’s fully working.

  • Handle the case where the game has ended already
  • Handle the case where the current player has no moves available

Both cases aren’t too complex to be handled with a few lines of codes. And then, there’s a performance concern: Why bother searching deeper when one move is bad enough that subsequent moves can’t perform any better?

Fortunately, with the help of the Alpha-Beta Pruning algorithm, this is a solvable problem, which ~I will describe in a later article~ I have written here.

Leave a comment