Created
March 18, 2016 10:38
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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