Skip to content

Instantly share code, notes, and snippets.

@dyerrington
Created March 18, 2016 10:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dyerrington/d08f30a051ad6daeb7ff to your computer and use it in GitHub Desktop.
Save dyerrington/d08f30a051ad6daeb7ff to your computer and use it in GitHub Desktop.
This is a quick framework for a game of connect4 that plays itself. While I was working on this, I've thought of some neat ways to write an AI that could be outfitted with a machine learning process perhaps.
import numpy as np, random, time, random, itertools
from itertools import cycle
class connect4:
# These values will override dynamic class attributes
defaults = {
'board_matrix_size': (7,6), # Standard connect-4 board size is 7x6
'game_simulate_steps': 3, # For testing
'game_simulate_delay': 1, # Delay between game simulation steps
'game_simulate_display': True, # Print out board between moves
'game_sequence_threashold': 4, # Number of player marker sequences to qualify winning condition
}
# Dynamic class attributes, can be set from instance creation / construction
game_sequence_threashold = None
game_simulate_display = None
game_simulate_delay = None
game_simulate_steps = None
board_matrix_size = None
# Static attributes used internally
board_matrix = None
def __init__(self, **options):
# Set default class attributes
for attribute, value in self.defaults.iteritems():
setattr(self, attribute, value)
# Set passed options / overwrite defaults
for attribute, value in options.iteritems():
setattr(self, attribute, value)
# Setup game board
self.set_board_matrix(self.board_matrix_size)
# initial player placement player 1 & 2 (X & O)
self.set_initial_player(player=1)
self.set_initial_player(player=2)
def set_board_matrix(self, size):
self.board_matrix = np.zeros(size)
## set_intial_player()
#
# This method will set the intial state of a player. In hindsight, I believe this isn't 100% nesessary but
# it was useful as a starting point to kick off development.
#
# @param player int
###
def set_initial_player(self, player):
# Check if valid position
while player not in self.board_matrix[-1]:
random_offset = random.randint(0, self.board_matrix_size[1] - 1)
if self.board_matrix[-1][random_offset] == 0:
self.board_matrix[-1][random_offset] = player
## simulate_game()
#
# This method controls the flow of the game, looking for possible moves, deciding which move is best (random)
# then checking for winning conditions (ending game).
#
# Largely, the paramters you initialze this class with control how this method behaves:
#
# - game_simulate_steps: Runs game with limited number of iterations
# - game_simulate_delay: Delay between iterations making it possible to view matrix updates
#
###
def simulate_game(self):
player_cycle = cycle((1,2))
current_player = 2
target_steps = self.game_simulate_steps if self.game_simulate_steps else self.board_matrix.size
for step in range(target_steps):
print "\nMove %d, Player %d\n--------------------------" % (step, current_player)
# Get possible moves
possible_moves = self.get_available_moves(player=current_player)
random_row_index, random_column_index = self.choose_random_move(possible_moves)
# Set player marker into position
self.set_player_marker(
row_index = random_row_index,
column_index = random_column_index,
player = current_player
)
# You can turn this off if you like but why would you!?
if self.game_simulate_display:
print self.board_matrix
winning_condition = self.is_winner(player = current_player)
if winning_condition:
print "Player %d has won by shear luck (unfortunately) @ move %d, with \"%s\" sequence!" % (current_player, step, winning_condition)
break
if self.game_simulate_delay:
time.sleep(self.game_simulate_delay)
current_player = next(player_cycle)
## is_player()
#
# This is a helper method to help comprehension a bit to avoid messy looking boolean blocks. Simply checks if the marked
# coordinate is a player marker or not.
#
# * Generally, I like to name my methods in a way that's self-documenting. is_* methods I like to return boolean type.
#
# @params row_index int, column_index int
# @return boolean
####
def is_player(self, row_index, column_index):
try:
if self.board_matrix.tolist()[row_index][column_index] in (1, 2):
return True
except:
pass
return False
## choose_random_move()
#
# Initally I attempted to create a method that finds all possible sequences that ended in a free "available" space,
# given the player markers are present and for horizontal, vertical, and diaganol directions. I quickly realized
# that writing most of the matrix hanlding logic by hand, is not something I can accomplish in a few short hours.
# In the interest of just getting something to work and show, I've opted for Numpy helpers when possible, and using
# this random placement method as a placeholder for a future time.
#
# In the previous file, connect4.py, there's some reference of my attempt if it's worthwhile. Otherwise, I've chosen
# to place random moves. Otherwise, I would find the longest sequence potential described previously to find the best
# move given player context.
#
# @param moves: dictionary indexed by rows and column offsets of available moves
# @returns tuple: random x/y coordinate
#
###
def choose_random_move(self, moves):
random_row = random.choice(moves.keys())
random_value = random.choice(moves[random_row])
return (random_row, random_value)
### set_player_marker()
#
# This is bascially a setter for player markers. I thought it might make the code easier to read and I might
# actually want to do something more with this in the future.
#
# @params row_index int, column_index int, player int
###
def set_player_marker(self, row_index, column_index, player = 1):
self.board_matrix[row_index, column_index] = player
## move_valid()
#
# This method checks a row_index and column_index (x/y) coordinate for either an edge, or a player
# to evaluate game rule conditions.
#
# @params row_index int, column_index int
# @return boolean
###
def move_valid(self, row_index, column_index):
# look for player below offset
next_row = row_index + 1
if self.is_player(row_index=next_row, column_index=column_index):
return True
# look for end of board boundary
if row_index == self.board_matrix_size[1]:
return True
return False
## is_sequence_threashold_met()
#
# I know this must seem like a very java-looking method but I think it pays to be descriptive so you don't
# have to think so hard next time you return to your code. The intention with this method is to be reusable.
#
# Standard segment length check.
#
# @params player int, target_list list, threashold int
# @return boolean
###
def is_sequence_threashold_met(self, player = 1, target_list = [], threashold=4):
if player in target_list:
longest_segment = max(sum(1 for _ in l) for marker, l in itertools.groupby(target_list) if marker == player)
if longest_segment >= threashold:
return True
return False
## is_winner()
#
# This method will check if a sequence of 4 (or self.game_sequence_threashold), can be found diagonally, vertically, or horizontally
# inside the matrix.
#
# The matrix methods used within, originally I attempted to write from scratch but I am not that badass I'm afriad :(
# To actually finish and ship "something" I opted to implement a method I was familliar with using Numpy.
#
# @param player int
# @return
###
def is_winner(self, player = 1):
# first - check horizontal
for row in self.board_matrix.tolist():
if self.is_sequence_threashold_met(player = player, target_list = row, threashold = self.game_sequence_threashold):
return "horizontal" # was going to go boolean but this might be easier to assess in simulation
# 2nd - vertical
for column in self.board_matrix.transpose().tolist():
if self.is_sequence_threashold_met(player = player, target_list = column, threashold = self.game_sequence_threashold):
return "vertical"
# 3rd - diags
for offset in range(self.board_matrix.shape[1] * -1, self.board_matrix.shape[1]):
# 3.1
# scan left to right diagonals, skipping iterations that are unecessary -- could be optimized
diag_left_to_right = self.board_matrix.diagonal(offset, 1, 0).tolist()
diag_right_to_left = self.board_matrix[::-1].diagonal(offset, 1, 0).tolist()
if len(diag_right_to_left) < self.game_sequence_threashold and len(diag_left_to_right) < self.game_sequence_threashold:
continue
if self.is_sequence_threashold_met(player = player, target_list = diag_left_to_right, threashold = self.game_sequence_threashold):
return "left to right diagonal"
if self.is_sequence_threashold_met(player = player, target_list = diag_right_to_left, threashold = self.game_sequence_threashold):
return "right to left diagonal"
# No one wins
return False
## get_available_moves
#
# Looks for all possible moves, given the current state of the board matrix.
#
# @param player int
# @return dict
###
def get_available_moves(self, player = 1):
available = {}
# eval down
for index, row in enumerate(self.board_matrix.tolist()):
row_available = [i for i, v in enumerate(row) if v == 0 and self.move_valid(index, i)]
if len(row_available):
available.update({index: row_available})
return available
# Intialize the game class
game = connect4(
game_simulate_steps=0, # set this to 0 if you want to simulate to game end conditions
game_simulate_delay=.5
)
game.simulate_game()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment