Last active
June 25, 2018 14:53
-
-
Save quadrupleslap/668be519b608df4ac575ed4e31ed0756 to your computer and use it in GitHub Desktop.
Command-line noughts and crosses.
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 random | |
BLANK = 0 | |
P1 = 1 | |
P2 = 2 | |
TRANSLATE = { BLANK: '.', P1: 'O', P2: 'X' } | |
def main(): | |
'''Play noughts and crosses!''' | |
# Configure the players. | |
p1_human = yesno('Player 1 human?', True) | |
p2_human = yesno('Player 2 human?', False) | |
print() | |
# Print some introduction text. | |
print('The match begins!') | |
print() | |
# Make a completely blank grid. | |
grid = [[BLANK] * 3 for _ in range(3)] | |
# Play the game. | |
while True: | |
play(grid, P1, p1_human) | |
winner = end(grid) | |
if winner is not None: break | |
play(grid, P2, p2_human) | |
winner = end(grid) | |
if winner is not None: break | |
# Print the final grid. | |
print_grid(grid) | |
# Announce the winner. | |
if winner == P1: | |
print('Player 1 wins!') | |
elif winner == P2: | |
print('Player 2 wins!') | |
else: | |
print('It\'s a draw!') | |
def print_grid(grid): | |
'''Display the grid to the user.''' | |
for row in grid: | |
print(' '.join(TRANSLATE[c] for c in row)) | |
print() | |
def play(grid, player, is_human): | |
'''Make a move.''' | |
if is_human : human(grid, player) | |
else : cpu(grid, player) | |
def cpu(grid, player): | |
'''Make a move that leads to the best worst-case scenario.''' | |
# Research 'minimax' for more information on this algorithm. | |
best_moves = [] | |
best_worst_case = float('-inf') | |
for y in range(3): | |
for x in range(3): | |
if grid[y][x] == BLANK: | |
grid[y][x] = player | |
wc = worst_case(grid, player) | |
if wc > best_worst_case: | |
best_moves = [(x, y)] | |
best_worst_case = wc | |
elif wc == best_worst_case: | |
best_moves.append((x, y)) | |
grid[y][x] = BLANK | |
bx, by = random.choice(best_moves) | |
grid[by][bx] = player | |
def worst_case(grid, target, player=None): | |
'''Get the worst-case scenario of the game, given its current state.''' | |
# Initially, we assume that the target has just moved. | |
if player is None: | |
player = opponent(target) | |
# If the game is over, there is only one case! | |
winner = end(grid) | |
if winner is not None: | |
if winner == target : return 2 | |
elif winner == BLANK : return 1 | |
else : return 0 | |
# If it's our (the target's) turn, we can choose the best move. | |
# Otherwise, assume that the opponent makes the best move for them, which, | |
# because we only have two players, is also the worst move for us. | |
if player == target : best, fun = float('-inf'), max | |
else : best, fun = float('inf'), min | |
for y in range(3): | |
for x in range(3): | |
if grid[y][x] == BLANK: | |
grid[y][x] = player | |
best = fun(best, worst_case(grid, target, opponent(player))) | |
grid[y][x] = BLANK | |
# A small optimization to return early at extrema. | |
if (best == 2 and fun is max) or (best == 0 and fun is min): | |
return best | |
return best | |
def human(grid, player): | |
'''Ask a human player to make a move.''' | |
# Create the key map and print the grid. | |
n = 0 | |
keymap = {} | |
for y, row in enumerate(grid): | |
out = [] | |
for x, cell in enumerate(row): | |
if cell == BLANK: | |
n += 1 | |
keymap[str(n)] = (x, y) | |
# The weird codes are for color. | |
# Research 'ANSI color codes' for more information. | |
out.append('\033[91m{}\033[0m'.format(n)) | |
else: | |
out.append(TRANSLATE[cell]) | |
print(' '.join(out)) | |
print() | |
# Select a message to show to the player. | |
message = random.choice([ | |
'Player {}, it\'s your turn.', | |
'Player {}, make your move!', | |
'Player {}, what will you do?', | |
'Good luck, player {}.', | |
'Let\'s see what you got, player {}.' | |
] if len(keymap) > 1 else [ | |
'This match is just about over, player {}.', | |
'This has been an interesting match, player {}!' | |
]).format('1' if player == P1 else '2') | |
# Ask the player for their choice of move, and make that move. | |
while True: | |
move = input('{} [1-{}] '.format(message, n)) | |
if move in keymap: | |
x, y = keymap[move] | |
grid[y][x] = player | |
break | |
print() | |
def end(grid): | |
'''Returns who won, None if it's not over, and BLANK if it's a draw.''' | |
for row in grid: | |
if row[0] != BLANK and row[0] == row[1] == row[2]: | |
return row[0] | |
for col in zip(*grid): | |
if col[0] != BLANK and col[0] == col[1] == col[2]: | |
return col[0] | |
if grid[0][0] != BLANK and grid[0][0] == grid[1][1] == grid[2][2]: | |
return grid[0][0] | |
if grid[0][2] != BLANK and grid[0][2] == grid[1][1] == grid[2][0]: | |
return grid[0][2] | |
return None if any(BLANK in row for row in grid) else BLANK | |
def opponent(player): | |
'''The other player.''' | |
return 3 - player | |
def yesno(q, default=False): | |
'''Pose to the user a yes-or-no question.''' | |
while True: | |
i = input('{} [{}] '.format(q, 'Yn' if default else 'yN')).lower() | |
if i == '' : return default | |
elif 'yes'.startswith(i) : return True | |
elif 'no'.startswith(i) : return False | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment