0

So I've implemented a game of connect four because I am making an AlphaZero clone and needed a simple game to test it on. However, I ran into some weird issues with the game code. Whenever a move is played, all future game objects are initialized with that new position (until another move is played on a Game object).

Here is my code.

from scipy.signal import convolve2d
from random import *
import numpy as np

class Game:
    '''the game being played'''

    def __init__(self, state=[[0]*6 for _ in range(7)], legalMoves=None, topSlots=None):
        '''Game(state=@default, legalMoves=@compute, topSlots=@compute) -> Game'''
        # attributes that every game must have
        self.state = state # initialize an empty board
        self.moves = list(range(7)) # the list of every possible move

        # list of legal moves
        if legalMoves != None:
            self.legalMoves = legalMoves
        else:
            self.legalMoves = [self.state[i][-1] == 0 for i in range(7)]
        
        # attributes specific to the game
        self.topSlots = topSlots if topSlots != None else self.find_top_slots()
        
    def legal_binary(self):
        '''Game.legal_binary() -> list
        returns the binary version of the
        list of legal moves'''
        return self.legalMoves
        
    def legal_moves(self):
        '''Game.legal_moves() -> list
        returns the list of precomputed legal moves.'''
        return [i for i in self.moves if self.legalMoves[i]]
    
    def moves(self):
        '''Game.get_moves() -> list
        returns the list of all possible (not necessarily legal) moves'''
        return self.moves()
        
    def find_top_slots(self):
        '''Game.find_top_slots() -> list
        finds and returns the list of top
        slots in the given state'''
        self.topSlots = [self.state[i].index(0) if not self.state[i][-1] else 7 for i in range(7)]
        return self.topSlots

    def is_terminal(self):
        '''Game.is_terminal() -> bool
        returns True if the current state
        is terminal; False otherwise'''
        return self.get_value() != None

    def get_value(self):
        '''Game.get_value() -> int/None
        returns the value of the board,
        if the game is terminal. ASSUMES
        THAT WE ARE CHECKING IMMEDIATELY
        AFTER A MOVE IS PLAYED & THE BOARD
        HAS NOT YET BEEN INVERTED'''
        
        # check for draw
        if self.legalMoves == [0]*7:
            # draw = 0 points
            return 0
        
        # check for wins
        state = np.array(self.state)

        horizKern = np.array([[1]*4])           # horizontal wins
        vertKern = np.transpose(horizKern)      # vertical wins
        diagKern1 = np.eye(4, dtype=np.uint8)   # diagonal win
        diagKern2 = np.fliplr(diagKern1)        # other diagonal win
        detectionKerns = [horizKern,vertKern,diagKern1,diagKern2]

        for kernel in detectionKerns:
            if (convolve2d(state, kernel, mode="valid") == 4).any():
                return 1 # the current player just played a winning move

        # no winner
        return None
        
    def play_move(self, move, invert=True):
        '''Game.play_move(move, invert=True) -> Game
        plays move and returns the game'''
        # ensure move is legal
        if self.legalMoves[move]:
            # invert board to update current player
            if invert:
                self.invert()
            
            # play the move
            self.state[move][self.topSlots[move]] = 1
            self.topSlots[move] += 1 # increase top slot

            # update legal moves
            self.legalMoves[move] = self.topSlots[move] != 6
        # return
        return self

    def invert(self):
        '''Game.invert() -> Game
        swaps the values of p1 and p2'''

        # multiply each item in the game by -1
        for i in range(7):
            for j in range(6):
                self.state[i][j] = -self.state[i][j]

        # return
        return self

    def __repr__(self):
        gamestr = ''
        for i in range(1,7):
            for j in range(7):
                gamestr += 'Y ' if self.state[j][-i] == 1 else 'R ' if self.state[j][-i] else '_ '
            gamestr += '\n'

        return gamestr

However, when I create a Game object and use the play_move() method, it changes the initialization of all game objects from then on:

>>> game = Game()
>>> game = Game()
>>> game
    _ _ _ _ _ _ _ 
    _ _ _ _ _ _ _ 
    _ _ _ _ _ _ _ 
    _ _ _ _ _ _ _ 
    _ _ _ _ _ _ _ 
    _ _ _ _ _ _ _ 

>>> game.play_move(3)
    _ _ _ _ _ _ _ 
    _ _ _ _ _ _ _ 
    _ _ _ _ _ _ _ 
    _ _ _ _ _ _ _ 
    _ _ _ _ _ _ _ 
    _ _ _ Y _ _ _ 

>>> game2 = Game()
>>> game2
    _ _ _ _ _ _ _ 
    _ _ _ _ _ _ _ 
    _ _ _ _ _ _ _ 
    _ _ _ _ _ _ _ 
    _ _ _ _ _ _ _ 
    _ _ _ Y _ _ _ 

Why is this happening, and how can I prevent it?

  • 1
    Because your parameter `state=[[0]*6 for _ in range(7)]` is *initialized once* when the function is defined and the same list is re-used. – juanpa.arrivillaga Sep 08 '22 at 15:41
  • I would suggest using numpy. Initialize `state` inside the constructor using `state=np.zeros((6,7))` –  Sep 08 '22 at 15:41

0 Answers0