Created
December 11, 2018 13:31
-
-
Save EdNutting/907f455ad4481f50a5c3faee6096155b to your computer and use it in GitHub Desktop.
Simple version of Peg Solitaire in Python
This file contains hidden or 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 os, sys | |
from enum import Enum | |
# Enumerables used to define machine-checkable values | |
# They also make the code more readable and flexible | |
# Stores possible inputs | |
class InputType(Enum): | |
QUIT = -1 # 'q' - Immediately quit the game | |
NONE = 0 # Blank input - indicates nothing | |
RESTART = 1 # 'r' - Immediately restart the game | |
INVALID = 2 # Invalid input - indicates user error | |
UNDO = 3 # 'u' - Undo the last move | |
VALUE = 4 # A valid value for X/Y/Direction | |
BACK = 5 # 'b' - Go back to previous entry step (X/Y/Dir) | |
# Stores current state of the game | |
class GameState(Enum): | |
PLAYING = 0 # The game continues... | |
LOSE = 1 # The game has been lost (undo still possible) | |
WIN = 2 # The game has been won (undo still possible) | |
QUIT = 3 # Quit the game at the next opportunity | |
class LocationState(Enum): | |
INVALID = -1 # A peg cannot be moved to an 'invalid' location | |
HOLE = 0 | |
PEG = 1 | |
class InputState(Enum): | |
X = 0 # Currently waiting for a valid X coordinate | |
Y = 1 # Currently waiting for a valid Y coordinate | |
DIR = 2 # Currently waiting for a valid direction | |
RESTART = 3 # Currently waiting for whether a new game should be started | |
class Direction(Enum): | |
NORTH = 0 | |
SOUTH = 1 | |
EAST = 2 | |
WEST = 3 | |
input_state = None # Input state | |
state = None # Game state | |
board = None # Board state | |
# Note: The board is padded by two rows/columns on all sides | |
# This eliminates the need for array bounds checks throughout | |
# the code. | |
move_state = (None, None, None) # Validated inputted move | |
past_boards = [] # Undo history for current game (stack) | |
# Create an entirely empty (all-invalid) board | |
def initEmptyBoard(): | |
board = [] | |
for i in range(0, 11): | |
board.append([]) | |
for j in range(0, 11): | |
board[i].append(LocationState.INVALID) | |
return board | |
# Given an existing board, set up the standard starting state | |
def initStartingBoard(board): | |
for i in range(2, 9): | |
if i < 4 or i > 6: | |
for j in range(4, 7): | |
board[i][j] = LocationState.PEG | |
else: | |
for j in range(2, 9): | |
board[i][j] = LocationState.PEG | |
board[5][5] = LocationState.HOLE | |
# Given one of the shorthand representations used in Winning/Losing board | |
# convert it to the proper Enumerable types | |
def convertBoard(board): | |
for i in range(0, 11): | |
for j in range(0, 11): | |
if board[i][j] == -1: | |
board[i][j] = LocationState.INVALID | |
elif board[i][j] == 0: | |
board[i][j] = LocationState.HOLE | |
elif board[i][j] == 1: | |
board[i][j] = LocationState.PEG | |
# Initialise a trivially winnable board | |
def initWinningBoard(): | |
board = [[-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], | |
[-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], | |
[-1, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1], | |
[-1, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1], | |
[-1, -1, 0, 0, 0, 0, 0, 0, 0, -1, -1], | |
[-1, -1, 0, 1, 1, 0, 0, 0, 0, -1, -1], | |
[-1, -1, 0, 0, 0, 0, 0, 0, 0, -1, -1], | |
[-1, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1], | |
[-1, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1], | |
[-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], | |
[-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], | |
] | |
convertBoard(board) | |
return board | |
# Initialise a trivially losing board | |
def initLosingBoard(): | |
board = [[-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], | |
[-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], | |
[-1, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1], | |
[-1, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1], | |
[-1, -1, 0, 0, 0, 0, 0, 0, 0, -1, -1], | |
[-1, -1, 0, 1, 1, 0, 0, 1, 0, -1, -1], | |
[-1, -1, 0, 0, 0, 0, 0, 0, 0, -1, -1], | |
[-1, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1], | |
[-1, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1], | |
[-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], | |
[-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], | |
] | |
convertBoard(board) | |
return board | |
# Clone the contents of a board | |
# Note: Normal assignment (x = y) with arrays uses 'referential' | |
# assignment - the reference to the array is copied, not the | |
# contents of the array. In this case, we have an array of | |
# arrays - so we have to create a new array of arrays with the | |
# values copied instead of the references copied. | |
def cloneBoard(board): | |
board_clone = [] | |
for i in range(0, 11): | |
board_clone.append(board[i].copy()) | |
return board_clone | |
# Make code more readable later on by creating helper functions | |
# that have easy-to-read (from left-to-right) names | |
def isHole(x, y): | |
global board | |
return board[y][x] == LocationState.HOLE | |
def isPeg(x, y): | |
global board | |
return board[y][x] == LocationState.PEG | |
# Initialise a fresh game state | |
def init(): | |
global state, past_boards, board, input_state, move_state | |
past_boards = [] | |
move_state = (None, None, None) | |
state = GameState.PLAYING | |
input_state = InputState.X | |
board = initEmptyBoard() | |
initStartingBoard(board) | |
# Uncomment one of these lines for testing the final stages | |
# of the game | |
# board = initWinningBoard() | |
# board = initLosingBoard() | |
# Read input from the user according to the current input state | |
# Note: Does not change the current input state | |
# Note: Minimal sanitisation of the data to get it into the right | |
# format for the evaluation code. This will pass on invalid | |
# input. | |
def readInput(): | |
global input_state | |
# Print relevant messages based on input state | |
if input_state == InputState.X: | |
print("X: ", end = '') | |
elif input_state == InputState.Y: | |
print("Y: ", end = '') | |
elif input_state == InputState.DIR: | |
print("N/S/E/W: ", end = '') | |
elif input_state == InputState.RESTART: | |
print("New game? Y/N: ", end = '') | |
else: | |
print("Error: Unknown input state.") | |
# Flushing normally happens after a newline | |
# but I stopped the newlines above so that | |
# the user inputs on the same line as the instructions | |
# So flush here to make the text appear | |
sys.stdout.flush() | |
# Read the text input from the user | |
input = sys.stdin.readline() | |
# Sanitise the text input - lower case and remove whitespace | |
input = input.lower().strip() | |
# Set up a default result | |
result = (InputType.NONE, None) | |
# Quit, Undo, Restart and Back inputs can happen at any time | |
if input == "q" or input == "quit": | |
result = (InputType.QUIT, None) | |
elif input == "u" or input == "undo": | |
result = (InputType.UNDO, None) | |
elif input == "r" or input == "restart": | |
result = (InputType.RESTART, None) | |
elif input == "b" or input == "back": | |
result = (InputType.BACK, None) | |
# Input for starting a new game or not | |
elif input_state == InputState.RESTART: | |
if input == "y" or input == "yes": | |
result = (InputType.VALUE, True) | |
elif input == "n" or input == "no": | |
result = (InputType.VALUE, False) | |
else: | |
result = (InputType.INVALID, input) | |
# Input for X or Y values | |
elif input_state == InputState.X or input_state == InputState.Y: | |
# Try to get an integer, if we fail, it's invalid input | |
try: | |
value = int(input) | |
result = (InputType.VALUE, value) | |
except: | |
result = (InputType.INVALID, input) | |
elif input_state == InputState.DIR: | |
# Try to get a valid direction, if we fail, it's invalid input | |
if input == "n" or input == "north": | |
result = (InputType.VALUE, Direction.NORTH) | |
elif input == "s" or input == "south": | |
result = (InputType.VALUE, Direction.SOUTH) | |
elif input == "e" or input == "east": | |
result = (InputType.VALUE, Direction.EAST) | |
elif input == "w" or input == "west": | |
result = (InputType.VALUE, Direction.WEST) | |
else: | |
result = (InputType.INVALID, input) | |
else: | |
# Possibly may have missed something in rapid development | |
# so add a fallback plan just in case! | |
print("Error: Unknown input state!") | |
result = (InputType.NONE, None) | |
return result | |
# Compute the cell that is 'dist' away from (x, y) in direction 'd' | |
def computeLocation(x, y, d, dist = 2): | |
if d == Direction.NORTH: | |
return (x, y - dist) | |
elif d == Direction.SOUTH: | |
return (x, y + dist) | |
elif d == Direction.EAST: | |
return (x + dist, y) | |
elif d == Direction.WEST: | |
return (x - dist, y) | |
else: | |
return (x, y) | |
# Determine whether a given location and direction | |
# constitute a valid move or not | |
def isValidMove(x, y, d): | |
# Check that the location 1 away is a peg | |
# and 2 away is a hole | |
l1 = computeLocation(x, y, d, dist = 1) | |
l2 = computeLocation(x, y, d, dist = 2) | |
return isPeg(x, y) and isPeg(l1[0], l1[1]) and isHole(l2[0], l2[1]) | |
# Determine whether a given location has a valid move | |
# i.e. contains a peg and there is a peg to jump over into a hole | |
def hasMove(x, y): | |
# Early escape - minor optimisation | |
if not isPeg(x, y): | |
return False | |
ok = False | |
# For each possible direction | |
for d in [Direction.NORTH, Direction.SOUTH, | |
Direction.EAST , Direction.WEST]: | |
# If any direction has a valid move then we are 'ok' | |
# hence 'or' together the results | |
ok = ok or isValidMove(x, y, d) | |
return ok | |
def oppositeDirection(d): | |
if d == Direction.NORTH: | |
return Direction.SOUTH | |
elif d == Direction.SOUTH: | |
return Direction.NORTH | |
elif d == Direction.EAST: | |
return Direction.WEST | |
elif d == Direction.WEST: | |
return Direction.EAST | |
# Determine whether the player could move into (x, y) from their | |
# currently selected move state | |
def couldMoveHere(x, y): | |
global move_state | |
for d in [Direction.NORTH, Direction.SOUTH, | |
Direction.EAST , Direction.WEST]: | |
l = computeLocation(x, y, d, dist = 2) | |
if (move_state[0] == None or l[0] == (move_state[0] + 2)) and \ | |
(move_state[1] == None or l[1] == (move_state[1] + 2)) and \ | |
isValidMove(l[0], l[1], oppositeDirection(d)): | |
return True | |
return False | |
# Print the current move state in a human-readable way | |
def printMove(): | |
global move_state | |
print("Current move: " + str(move_state[0]) + ", " + str(move_state[1])) | |
# Print the current board with X/Y coordinates in a human-readable way | |
# Note: Use standard unicode characters for circles (I Googled for them) | |
# to nicely represent the pegs and holes | |
# (https://unicode-search.net/unicode-namesearch.pl?term=CIRCLE) | |
def printBoard(board): | |
# Print X-coords across the top | |
print(" ", end = '') | |
for i in range(2, 9): | |
print(str(i - 2) + " ", end = '') | |
print() | |
for i in range(2, 9): | |
# Print Y-coord for current row | |
print(str(i - 2) + " ", end = '') | |
# Print the current row of the board | |
for j in range(2, 9): | |
if board[i][j] == LocationState.INVALID: | |
print(" ", end = '') | |
elif isHole(j, i): | |
if couldMoveHere(j, i): | |
print("◌ ", end = '') | |
else: | |
print("○ ", end = '') | |
elif isPeg(j, i): | |
print("● ", end = '') | |
# Go to next row | |
print() | |
# Undo a move by restoring the board, game, move and input states | |
def undoMove(): | |
global state, past_boards, board, input_state, move_state | |
if len(past_boards) > 0: | |
board = past_boards.pop() | |
state = GameState.PLAYING | |
move_state = (None, None, None) | |
input_state = InputState.X | |
else: | |
print("No more moves to undo!") | |
pass | |
# Evaluate a move | |
# Note: This assumes the move is valid | |
# Validation is performed separately before this is called | |
def evalMove(): | |
global state, past_boards, board, input_state, move_state | |
# Store a copy of the current board for the undo history | |
past_boards.append(cloneBoard(board)) | |
# Extract x, y and direction values | |
x = move_state[0] + 2 | |
y = move_state[1] + 2 | |
d = move_state[2] | |
# Compute new locations for holes and pegs | |
newHoleLoc1 = computeLocation(x, y, d, dist = 1) | |
newHoleLoc2 = (x, y) | |
newPegLoc = computeLocation(x, y, d, dist = 2) | |
# Update the board state | |
board[newHoleLoc1[1]][newHoleLoc1[0]] = LocationState.HOLE | |
board[newHoleLoc2[1]][newHoleLoc2[0]] = LocationState.HOLE | |
board[newPegLoc[1]][newPegLoc[0]] = LocationState.PEG | |
# Determine how many pegs are left | |
# And if any valid moves exist | |
pegCount = 0 | |
validMoves = False | |
for i in range(2, 9): | |
for j in range(2, 9): | |
if isPeg(j, i): | |
pegCount += 1 | |
validMoves = validMoves or hasMove(j, i) | |
# Did the user win the game? | |
if pegCount == 1: | |
state = GameState.WIN | |
input_state = InputState.RESTART | |
# Did the user lose the game? | |
elif validMoves == 0: | |
state = GameState.LOSE | |
input_state = InputState.RESTART | |
# Check the currently inputted move state to see | |
# if it is going to be or is a valid move or not | |
# Note: +2 in this function is because the board | |
# is padded with 2 rows/columns of 'invalid' | |
# cells - this eliminates the needs for bounds | |
# checks when accessing the array which makes the | |
# code cleaner. We also use this technique for | |
# high performance computing code! (Scatter/gather) | |
def validateMoveState(): | |
global state, board, input_state, move_state | |
# Extract x, y and direction variables | |
x = move_state[0] | |
y = move_state[1] | |
d = move_state[2] | |
# No input yet | |
if x == None: | |
return True | |
# X has been inputted | |
# - Check x is in range | |
# - Check at least one valid move exists in that column | |
elif y == None: | |
if x >= 0 and x < 7: | |
ok = False | |
for y in range(0, 7): | |
ok = ok or hasMove(x + 2, y + 2) | |
return ok | |
else: | |
return False | |
# X and Y have been inputted | |
# - Check x and y are in range | |
# - Check at least one valid move exists for this location | |
elif d == None: | |
if x >= 0 and x < 7 and y >= 0 and y < 7: | |
return hasMove(x + 2, y + 2) | |
else: | |
return False | |
# X, Y and Direction have been inputted | |
# - Check x and y are in range | |
# - Check this is a valid move to make | |
else: | |
if x >= 0 and x < 7 and y >= 0 and y < 7: | |
loc = computeLocation(x + 2, y + 2, d) | |
return isValidMove(x + 2, y + 2, d) | |
else: | |
return False | |
# Evaluate a step of the playing state of the game | |
def evalPlayingState(input_type, input_value): | |
global state, board, input_state, move_state | |
# Undo the previous move - can be asked for at any input stage | |
if input_type == InputType.UNDO: | |
undoMove() | |
# Go back to previous input stage | |
# Note: Clear out last inputted value as well | |
elif input_type == InputType.BACK: | |
if input_state == InputState.X: | |
input_state = InputState.X | |
move_state = (None, None, None) | |
elif input_state == InputState.Y: | |
input_state = InputState.X | |
move_state = (None, None, None) | |
elif input_state == InputState.DIR: | |
input_state = InputState.Y | |
move_state = (move_state[0], None, None) | |
# Consume an inputted value | |
elif input_type == InputType.VALUE: | |
# X coordinate | |
if input_state == InputState.X: | |
move_state = (input_value, None, None) | |
if (validateMoveState()): | |
input_state = InputState.Y | |
else: | |
move_state = (None, None, None) | |
print("Invalid location or no available moves.") | |
# Y coordinate | |
elif input_state == InputState.Y: | |
move_state = (move_state[0], input_value, None) | |
if (validateMoveState()): | |
input_state = InputState.DIR | |
else: | |
move_state = (move_state[0], None, None) | |
print("Invalid move or no available moves. Please select elsewhere or use 'b' to go back.") | |
# Direction | |
elif input_state == InputState.DIR: | |
move_state = (move_state[0], move_state[1], input_value) | |
if (validateMoveState()): | |
# If the whole thing is a valid move, | |
# evaluate the move and go to the next move (or whatever evalMove sets up) | |
input_state = InputState.X | |
evalMove() | |
move_state = (None, None, None) | |
else: | |
move_state = (move_state[0], move_state[1], None) | |
print("Invalid move. Please select elsewhere or use 'b' to go back.") | |
# Process the winning or losing states | |
# Basically: Wait to see if they want to undo, restart or quit | |
def evalEndOfGameState(input_type, input_value): | |
global state | |
if input_type == InputType.RESTART: | |
init() | |
if input_type == InputType.VALUE: | |
if input_value: | |
init() | |
else: | |
state = GameState.QUIT | |
elif input_type == InputType.UNDO: | |
undoMove() | |
else: | |
print("Invalid input '" + input_value + "'. Please enter 'yes', 'no', 'undo' or 'restart'.") | |
# Evaluate the game state | |
def evalState(input): | |
global state, board, input_state, move_state | |
# Extract the input type and value | |
input_type = input[0] | |
input_value = input[1] | |
# Quit, Invalid and Restart could happen at any input stage | |
if input_type == InputType.QUIT: | |
state = GameState.QUIT | |
return | |
elif input_type == InputType.INVALID: | |
print("Invalid input '" + input_value + "'. Please try again.") | |
return | |
elif input_type == InputType.RESTART: | |
init() | |
return | |
# Execute the state transitions | |
if state == GameState.PLAYING: | |
evalPlayingState(input_type, input_value) | |
elif state == GameState.LOSE: | |
evalEndOfGameState(input_type, input_value) | |
elif state == GameState.WIN: | |
evalEndOfGameState(input_type, input_value) | |
else: | |
# In case during rapid development we missed something... | |
print("Erm...unknown game state - the programmer of this game messed up!") | |
# Print output as appropriate for the current game state | |
def printOutput(): | |
global state, board | |
print("") | |
if state == GameState.PLAYING: | |
printMove() | |
if state == GameState.LOSE: | |
print("You lost.") | |
if state == GameState.WIN: | |
print("You won!") | |
print() | |
printBoard(board) | |
print() | |
# The main read-eval-print loop (REPL) | |
# Note: The actual order given is print-read-eval (a rotation of REPL) | |
# This is so that a new game starts immediately when the app is first opened | |
def main(): | |
global state | |
print ("Welcome to the game!") | |
init() | |
input = (InputType.NONE, None) | |
# Clear the console using a (Windows) system call | |
os.system('cls') | |
while (state != GameState.QUIT): | |
printOutput() | |
input = readInput() | |
# Clear the console using a (Windows) system call | |
os.system('cls') | |
evalState(input) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment