Games implemented with easyAI

Here are reported some games implementations provided in the examples folder of easyAI.

Tic-Tac-Toe

from easyAI import TwoPlayersGame
from easyAI.Player import Human_Player

class TicTacToe( TwoPlayersGame ):
    """ The board positions are numbered as follows:
            7 8 9
            4 5 6
            1 2 3
    """    

    def __init__(self, players):
        self.players = players
        self.board = [0 for i in range(9)]
        self.nplayer = 1 # player 1 starts.
    
    def possible_moves(self):
        return [i+1 for i,e in enumerate(self.board) if e==0]
    
    def make_move(self, move):
        self.board[int(move)-1] = self.nplayer

    def unmake_move(self, move): # optional method (speeds up the AI)
        self.board[int(move)-1] = 0
    
    def lose(self):
        """ Has the opponent "three in line ?" """
        return any( [all([(self.board[c-1]== self.nopponent)
                      for c in line])
                      for line in [[1,2,3],[4,5,6],[7,8,9], # horiz.
                                   [1,4,7],[2,5,8],[3,6,9], # vertical
                                   [1,5,9],[3,5,7]]]) # diagonal
        
    def is_over(self):
        return (self.possible_moves() == []) or self.lose()
        
    def show(self):
        print ('\n'+'\n'.join([
                        ' '.join([['.','O','X'][self.board[3*j+i]]
                        for i in range(3)])
                 for j in range(3)]) )
                 
    def scoring(self):
        return -100 if self.lose() else 0
    

if __name__ == "__main__":
    
    from easyAI import AI_Player, Negamax
    ai_algo = Negamax(6)
    TicTacToe( [Human_Player(),AI_Player(ai_algo)]).play()

Game of Nim

from easyAI import TwoPlayersGame


class Nim(TwoPlayersGame):
    """
    The game starts with 4 piles of 5 pieces. In turn the players
    remove as much pieces as they want, but from one pile only. The
    player that removes the last piece loses.
    
    Arguments:
    
    - players: the players...
    - piles: the piles the game starts with. With piles=[2,3,4,4] the
      game will start with 1 pile of 2 pieces, 1 pile of 3 pieces, and 2
      piles of 4 pieces. If set to None, the default will be [5,5,5,5]
      
    """

    def __init__(self, players, piles = None):
        """ Default for `piles` is 5 piles of 5 pieces. """
        self.players = players
        self.piles = piles if (piles != None) else [5, 5, 5, 5]
        self.nplayer = 1 # player 1 starts.

    def possible_moves(self):
        return ["%d,%d" % (i + 1, j) for i in range(len(self.piles))
                for j in range(1, self.piles[i] + 1)]

    def make_move(self, move):
        move = list(map(int, move.split(',')))
        self.piles[move[0] - 1] -= move[1]

    def unmake_move(self, move): # optional, speeds up the AI
        move = list(map(int, move.split(',')))
        self.piles[move[0] - 1] += move[1]

    def show(self): print(" ".join(map(str, self.piles)))

    def win(self): return max(self.piles) == 0

    def is_over(self): return self.win()

    def scoring(self): return 100 if self.win() else 0

    def ttentry(self): return tuple(self.piles) #optional, speeds up AI


if __name__ == "__main__":
    # IN WHAT FOLLOWS WE SOLVE THE GAME AND START A MATCH AGAINST THE AI

    from easyAI import AI_Player, Human_Player, Negamax, id_solve
    from easyAI.AI import TT
    # we first solve the game
    w, d, m, tt = id_solve(Nim, range(5, 20), win_score = 80)
    print
    w, d, len(tt.d)
    # the previous line prints -1, 16 which shows that if the
    # computer plays second with an AI depth of 16 (or 15) it will
    # always win in 16 (total) moves or less.

    # Now let's play (and lose !) against the AI
    ai = Negamax(16, tt = TT())
    game = Nim([Human_Player(), AI_Player(tt)])
    game.play() # You will always lose this game !
    print("player %d wins" % game.nplayer)

    # Note that with the transposition table tt generated by id_solve
    # we can setup a perfect AI which doesn't have to think:
    # >>> game = Nim( [ Human_Player(), AI_Player( tt )])
    # >>> game.play() # You will always lose this game too!

Game of Knights

import numpy as np
from easyAI import TwoPlayersGame


# directions in which a knight can move
DIRECTIONS = list(map(np.array, [[1, 2], [-1, 2], [1, -2], [-1, -2],
                            [2, 1], [2, -1], [-2, 1], [-2, -1]]))

# functions to convert "D8" into (3,7) and back...
pos2string = lambda ab: "ABCDEFGH"[ab[0]] + str(ab[1] + 1)
string2pos = lambda s: np.array(["ABCDEFGH".index(s[0]), int(s[1])-1])


class Knights(TwoPlayersGame):
    """
    Each player has a chess knight (that moves in "L") on a chessboard.
    Each turn the player moves the knight to any tile that hasn't been
    occupied by a knight before. The first player that cannot move loses.
    """

    def __init__(self, players, board_size = (8, 8)):
        self.players = players
        self.board_size = board_size
        self.board = np.zeros(board_size, dtype = int)
        self.board[0, 0] = 1
        self.board[board_size[0] - 1, board_size[1] - 1] = 2
        players[0].pos = np.array([0, 0])
        players[1].pos = np.array([board_size[0] - 1, board_size[1]-1])
        self.nplayer = 1 # player 1 starts.

    def possible_moves(self):
        endings = [self.player.pos + d for d in DIRECTIONS]
        return [pos2string(e) for e in endings # all positions
                if (e[0] >= 0) and (e[1] >= 0) and
                   (e[0] < self.board_size[0]) and
                   (e[1] < self.board_size[1]) and # inside the board
                   self.board[e[0], e[1]] == 0] # and not blocked

    def make_move(self, pos):
        pi, pj = self.player.pos
        self.board[pi, pj] = 3 # 3 means blocked
        self.player.pos = string2pos(pos)
        pi, pj = self.player.pos
        self.board[pi, pj] = self.nplayer # place player on board

    def ttentry(self):
        e = [tuple(row) for row in self.board]
        e.append(pos2string(self.players[0].pos))
        e.append(pos2string(self.players[1].pos))
        return tuple(e)

    def ttrestore(self, entry):
        for x, row in enumerate(entry[:self.board_size[0]]):
            for y, n in enumerate(row):
                self.board[x, y] = n
        self.players[0].pos = string2pos(entry[-2])
        self.players[1].pos = string2pos(entry[-1])

    def show(self):
        print('\n' + '\n'.join(['  1 2 3 4 5 6 7 8'] +
              ['ABCDEFGH'[k] + 
               ' ' + ' '.join([['.', '1', '2', 'X'][self.board[k, i]]
               for i in range(self.board_size[0])])
               for k in range(self.board_size[1])] + ['']))

    def lose(self):
        return self.possible_moves() == []

    def scoring(self):
        return -100 if (self.possible_moves() == []) else 0

    def is_over(self):
        return self.lose()


if __name__ == "__main__":
    from easyAI import AI_Player, Negamax

    ai_algo = Negamax(11)
    game = Knights([AI_Player(ai_algo), AI_Player(ai_algo)], (5, 5))
    game.play()
    print("player %d loses" % (game.nplayer))

Awele

try:
    import numpy as np
except ImportError:
    print("Sorry, this example requires Numpy installed !")
    raise

from easyAI import TwoPlayersGame


class Awele(TwoPlayersGame):
    """
    Rules are as defined as in http://en.wikipedia.org/wiki/Oware
    with the additional rule that the game ends when then are 6 seeds
    left in the game.
    """

    def __init__(self, players):
        for i, player in enumerate(players):
            player.score = 0
            player.isstarved = False
            player.camp = i
        self.players = players
        
        # Initial configuration of the board.
        # holes are indexed by a,b,c,d...
        self.board = [4, 4, 4, 4, 4, 4,  
                      4, 4, 4, 4, 4, 4]  
                      
        self.nplayer = 1  # player 1 starts.

    def make_move(self, move):
        if move == "None":
            self.player.isstarved = True
            s = 6 * self.opponent.camp
            self.player.score += sum(self.board[s:s + 6])
            return

        move = 'abcdefghijkl'.index(move)

        pos = move
        for i in range(self.board[move]):  #DEAL
            pos = (pos + 1) % 12
            if pos == move:
                pos = (pos + 1) % 12
            self.board[pos] += 1

        self.board[move] = 0

        while ((pos / 6) == self.opponent.camp
               and (self.board[pos] in [2, 3])):  # TAKE
            self.player.score += self.board[pos]
            self.board[pos] = 0
            pos = (pos - 1) % 12

    def possible_moves(self):
        """
        A player must play any hole that contains enough seeds to
        'feed' the opponent. This no hole has this many seeds, any
        non-empty hole can be played.
        """

        if self.nplayer == 1:
            if max(self.board[:6]) == 0: return ['None']
            moves = [i for i in range(6) if (self.board[i] >= 6 - i)]
            if moves == []:
                moves = [i for i in range(6) if self.board[i] != 0]
        else:
            if max(self.board[6:]) == 0: return ['None']
            moves = [i for i in range(6,12) if (self.board[i] >= 12-i)]
            if moves == []:
                moves = [i for i in range(6, 12) if self.board[i] != 0]

        return ['abcdefghijkl'[u] for u in moves]

    def show(self):
        """ Prints the board, with the hole's respective letters """

        print("Score: %d / %d" % tuple(p.score for p in self.players))
        print('  '.join('lkjihg'))
        print(' '.join(["%02d" % i for i in self.board[-1:-7:-1]]))
        print(' '.join(["%02d" % i for i in self.board[:6]]))
        print('  '.join('abcdef'))

    def lose(self):
        return self.opponent.score > 24

    def is_over(self):
        return ( self.lose() or
                  sum(self.board) < 7 or
                  self.opponent.isstarved )


if __name__ == "__main__":
    # In what follows we setup the AI and launch a AI-vs-AI match.

    from easyAI import Human_Player, AI_Player, Negamax

    # this shows that the scoring can be defined in the AI algo, 
    # which enables 2 AIs with different scorings to play a match.
    scoring = lambda game: game.player.score - game.opponent.score
    ai = Negamax(6, scoring)
    game = Awele([AI_Player(ai), AI_Player(ai)])

    game.play()

    if game.player.score > game.opponent.score:
        print("Player %d wins." % game.nplayer)
    elif game.player.score < game.opponent.score:
        print("Player %d wins." % game.nopponent)
    else:
        print("Looks like we have a draw.")

Connect 4

try:
    import numpy as np
except ImportError:
    print("Sorry, this example requires Numpy installed !")
    raise

from easyAI import TwoPlayersGame


class ConnectFour(TwoPlayersGame):
    """
    The game of Connect Four, as described here:
    http://en.wikipedia.org/wiki/Connect_Four
    """

    def __init__(self, players, board = None):
        self.players = players
        self.board = board if (board != None) else (
            np.array([[0 for i in range(7)] for j in range(6)]))
        self.nplayer = 1 # player 1 starts.

    def possible_moves(self):
        return [i for i in range(7) if (self.board[:, i].min() == 0)]

    def make_move(self, column):
        line = np.argmin(self.board[:, column] != 0)
        self.board[line, column] = self.nplayer

    def show(self):
        print('\n' + '\n'.join(
                        ['0 1 2 3 4 5 6', 13 * '-'] +
                        [' '.join([['.', 'O', 'X'][self.board[5 - j][i]]
                        for i in range(7)]) for j in range(6)]))

    def lose(self):
        return find_four(self.board, self.nopponent)

    def is_over(self):
        return (self.board.min() > 0) or self.lose()

    def scoring(self):
        return -100 if self.lose() else 0


def find_four(board, nplayer):
    """
    Returns True iff the player has connected  4 (or more)
    This is much faster if written in C or Cython
    """
    for pos, direction in POS_DIR:
        streak = 0
        while (0 <= pos[0] <= 5) and (0 <= pos[1] <= 6):
            if board[pos[0], pos[1]] == nplayer:
                streak += 1
                if streak == 4:
                    return True
            else:
                streak = 0
            pos = pos + direction
    return False


POS_DIR = np.array([[[i, 0], [0, 1]] for i in range(6)] +
                   [[[0, i], [1, 0]] for i in range(7)] +
                   [[[i, 0], [1, 1]] for i in range(1, 3)] +
                   [[[0, i], [1, 1]] for i in range(4)] +
                   [[[i, 6], [1, -1]] for i in range(1, 3)] +
                   [[[0, i], [1, -1]] for i in range(3, 7)])

if __name__ == '__main__':
    # LET'S PLAY !

    from easyAI import Human_Player, AI_Player, Negamax, SSS, DUAL

    ai_algo_neg = Negamax(5)
    ai_algo_sss = SSS(5)
    game = ConnectFour([AI_Player(ai_algo_neg), AI_Player(ai_algo_sss)])
    game.play()
    if game.lose():
        print("Player %d wins." % (game.nopponent))
    else:
        print("Looks like we have a draw.")