#!/usr/bin/env python """ a Tic Tac Toe game in Python using classes for an intro programming demo. Jim Mahoney, Marlboro College; GPL copyright Oct 2006 """ import sys # For sys.exit() import random # For random.randint(low,high) def letter2point(letter): """ Convert a letter 'a'...'i' to a point (0,0) to (2,2). """ zero_to_eight = ord(letter) - ord('a') row = zero_to_eight / 3 # 0, 0, 0, 1, 1, 1, 2, 2, 2 column = zero_to_eight % 3 # 0, 1, 2, 0, 1, 2, 0, 1, 2 return (row, column) def point2letter(point): """ Convert a point (0,0) to (2,2) to a letter 'a' ... 'i'. """ (row, column) = point return chr(ord('a') + row*3 + column) def ask(question, legal_responses=()): """ Get input from the user. If the first letter is 'q' (for 'quit'), quit the program. A list of legal responses (first letters) may be specified; if so, keep asking until one of those is seen. """ while True: answer = raw_input(question) if (answer): first_char = answer[0] else: first_char = "" if (first_char == 'q'): print "\n Bye. " sys.exit() elif (first_char in legal_responses or not legal_responses): return answer else: print " Oops: first letter isn't", legal_responses + ('q',), "." print " Please try again." class Person: """ An interactive person player. """ def __init__(self, symbol): self.symbol = symbol def get_symbol(self): return self.symbol def get_move(self, board): """ Return a (row,column) point where the person wants to move. """ while True: letter = ask( " " + self.symbol + ": Where do you want to move? ", ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i')) move = letter2point(letter) if (board.get_symb_at(move) != ' '): print " Oops: there's already a mark there. Try again." else: print return move class Computer: """ A (random) computer player. """ def __init__(self, symbol): self.symbol = symbol def get_symbol(self): return self.symbol def get_move(self, board): """ Get the computer's next (random) move as a (row,column) point. """ x = random.randint(0, board.get_empty_count()-1) move = board.get_nth_empty_move(x) letter = point2letter(move) print " " + self.symbol + ": the computer chooses '"+letter+ "'." print return move class Board: """ The game board. """ # All the ways to win : directions = ( ( (0,0), (0,1), (0,2) ), # horizontals, ( (1,0), (1,1), (1,2) ), ( (2,0), (2,1), (2,2) ), ( (0,0), (1,0), (2,0) ), # verticals, ( (0,1), (1,1), (2,1) ), ( (0,2), (1,2), (2,2) ), ( (0,0), (1,1), (2,2) ), # and diagonals. ( (0,2), (1,1), (2,0) ) ) def __init__(self): self.grid = [ [' ']*3, [' ']*3, [' ']*3 ] # 3x3 matrix of spaces self.empty = 9 # remaining empty spots def get_empty_count(self): return self.empty def __str__(self): """ What a Board should look like when converted to a string. """ return self.row2string(0) + " a | b | c \n" + \ " ---+---+--- ---+---+---\n" + \ self.row2string(1) + " d | e | f \n" + \ " ---+---+--- ---+---+---\n" + \ self.row2string(2) + " g | h | i \n" def row2string(self, row): """ Return one row of the board as a string. """ return " " + self.grid[row][0] + \ " | " + self.grid[row][1] + \ " | " + self.grid[row][2] def make_move(self, move, symbol): """ Make a move on the board. """ (row, column) = move self.grid[row][column] = symbol self.empty = self.empty - 1 def get_symb_at(self, point): """ Given a (row,col) point, return the symbol on the grid there. """ (row, col) = point return self.grid[row][col] def get_nth_empty_move(self, n_want): """ Return (row,col) point of the n'th (0,1,2,...) empty spot.""" n_seen = 0 for row in range(3): for col in range(3): if (self.grid[row][col] == ' '): if (n_want == n_seen): return (row,col) else: n_seen = n_seen +1 print " *** " print " OOPS - asked for empty ", n_want, " on board \n", self.grid print " *** " return (-1,-1) # error def get_winner(self): """ Return 'X', 'O', 'tie', or (if the game isn't done) just ''. """ for line in self.directions: symbol = self.get_symb_at(line[0]) if (symbol in ('X','O') and \ symbol==self.get_symb_at(line[1])==self.get_symb_at(line[2])): return symbol if (self.empty == 0): return "tie" else: return '' class Game: """ Choose players and play a game. """ def __init__(self, players=[]): """ Start a new game with the given players, or prompt for the players if not provided.""" print print " == Playing a new game. ==" if (players): self.players = players else: self.players = [ self.make_player('X'), self.make_player('O') ] self.whose_turn = 0 self.board = Board() def make_player(self, symbol): """ Prompt user for a person or computer player for X or O. """ answer = ask(" Player " + symbol + " is person or computer ? ", legal_responses = ('p','c') ) if (answer[0]=='p'): return Person(symbol) else : return Computer(symbol) def play(self): """ Play a game. """ winner = '' print while (not winner): print self.board player = self.players[self.whose_turn] next_move = player.get_move(self.board) self.board.make_move(next_move, player.get_symbol()) winner = self.board.get_winner() self.whose_turn = (self.whose_turn + 1) % 2 if (winner in ('X', 'O')): print self.board print " ", winner, " wins!" else: print self.board print " Tie game." class Referee: """ Manage a series of games. """ def run(self): print " === Tic Tac Toe === " print " Inputs may be abbreviated by their first letter. " print " Type 'quit' at any prompt to quit. " print " X always goes first." print " So there." while True: Game().play() Referee().run() """ === discussion questions ====================================== * Do you think that all of this should be done with objects? * How about Referee? * Why are the parens different in the following : import math x = math.sqrt(10.0) vs Referee().run() ? * What's going on with this line : self.player_X = player_X or self.get_player('X') * What are these special methods doing ? object.__init__() ? object.__str__() ? * What is this line doing? letter = chr(ord('a') + move[0]*3 + move[1]) * Does the Game object really need to have (whose_move, board) as data within it? Or could those just be local variables in the Game.play() function? What are the advantages / disadvantages of each? Below is a summary of the functions, objects, data, and methods. Some of these are intended to be 'public' (what I'd call the 'API') and some are 'private' (only for use within that class). * Which of these are data? Which methods? * Where are they defined in the code above? * Is everything obvious from just the names? * Or are some properties implied? * Did I use getter's and setter's consistently everywhere? * Where do you think I cut corners? We'll be talking about 'inherited' classes later, for example class Animal: "parent class" class Dog(Animal): "child class, inherits from Animal" class Cat(Animal): "child class, inherits from Animal" Are there any candidates for that here? letter2point(letter) point2letter(point) ask(question, legal_responses) Person(symbol) .symbol .get_symbol() .get_move(board) Computer(symbol) .symbol .get_symbol() .get_move(board) Board() .directions .empty .grid .row2string(row) .get_empty_count() .get_symb_at(point) .get_nth_empty_move(n) .make_move(move, symbol) .get_winner() Game() .whose_turn .players .board .make_player(symbol) .play() Referee() .run() Other remarks : * I haven't included a good test suite here; I just tried various things and eyeballed it. Several bugs persisted for a while: * It wasn't finding 'O' wins. (I was looking for "in ('X','Y')" * It was calling anything with 9 moves a tie. * The random thing worked some of the time ... countin logic was wrong. * The user could mark over an already place mark and cheat. (Oops). * Hitting return without typing crashed when answer[0] was examined. There is at least one bug left. Can you find it? (I've put it at the end of this file...) * What do you think some of the tests should be? * All in all this was one full evening's worth of coding; about six hours - and I've written tic-tac-toe programs before in other languages. So you should be aware that doing anything of this scope (which isn't all that big as programming project go) takes time and will have bugs. * Suppose we want to make the computer *win* . How do we go about writing that? Ah ... the *next* project for Jim ... == sample output ===================================================== $ ./tic-tac-toe.py === Tic Tac Toe === Inputs may be abbreviated by their first letter. Type 'quit' at any prompt to quit. X always goes first. So there. == Playing a new game. == Player X is person or computer ? p Player O is person or computer ? c | | a | b | c ---+---+--- ---+---+--- | | d | e | f ---+---+--- ---+---+--- | | g | h | i X: Where do you want to move? a X | | a | b | c ---+---+--- ---+---+--- | | d | e | f ---+---+--- ---+---+--- | | g | h | i O: the computer chooses 'b'. X | O | a | b | c ---+---+--- ---+---+--- | | d | e | f ---+---+--- ---+---+--- | | g | h | i X: Where do you want to move? e X | O | a | b | c ---+---+--- ---+---+--- | X | d | e | f ---+---+--- ---+---+--- | | g | h | i O: the computer chooses 'c'. X | O | O a | b | c ---+---+--- ---+---+--- | X | d | e | f ---+---+--- ---+---+--- | | g | h | i X: Where do you want to move? i X | O | O a | b | c ---+---+--- ---+---+--- | X | d | e | f ---+---+--- ---+---+--- | | X g | h | i X wins! == Playing a new game. == Player X is person or computer ? c Player O is person or computer ? c | | a | b | c ---+---+--- ---+---+--- | | d | e | f ---+---+--- ---+---+--- | | g | h | i X: the computer chooses 'f'. | | a | b | c ---+---+--- ---+---+--- | | X d | e | f ---+---+--- ---+---+--- | | g | h | i O: the computer chooses 'd'. | | a | b | c ---+---+--- ---+---+--- O | | X d | e | f ---+---+--- ---+---+--- | | g | h | i X: the computer chooses 'i'. | | a | b | c ---+---+--- ---+---+--- O | | X d | e | f ---+---+--- ---+---+--- | | X g | h | i O: the computer chooses 'g'. | | a | b | c ---+---+--- ---+---+--- O | | X d | e | f ---+---+--- ---+---+--- O | | X g | h | i X: the computer chooses 'e'. | | a | b | c ---+---+--- ---+---+--- O | X | X d | e | f ---+---+--- ---+---+--- O | | X g | h | i O: the computer chooses 'a'. O | | a | b | c ---+---+--- ---+---+--- O | X | X d | e | f ---+---+--- ---+---+--- O | | X g | h | i X: the computer chooses 'h'. O | | a | b | c ---+---+--- ---+---+--- O | X | X d | e | f ---+---+--- ---+---+--- O | X | X g | h | i O: the computer chooses 'c'. O | | O a | b | c ---+---+--- ---+---+--- O | X | X d | e | f ---+---+--- ---+---+--- O | X | X g | h | i X: the computer chooses 'b'. O | X | O a | b | c ---+---+--- ---+---+--- O | X | X d | e | f ---+---+--- ---+---+--- O | X | X g | h | i Tie game. == Playing a new game. == Player X is person or computer ? q Bye. $ """ #### # A bug I haven't fixed yet ... # # X: Where do you want to move? a;ldkjfa # Traceback (most recent call last): # File "./tic-tac-toe.py", line 206, in ? # Referee().run() # File "./tic-tac-toe.py", line 204, in run # Game().play() # File "./tic-tac-toe.py", line 184, in play # next_move = player.get_move(self.board) # File "./tic-tac-toe.py", line 57, in get_move # move = letter2point(letter) # File "./tic-tac-toe.py", line 13, in letter2point # zero_to_eight = ord(letter) - ord('a') # TypeError: ord() expected a character, but string of length 8 found #