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?