Royal Game of Ur
The Royal Game of Ur is a 5,000-year-old game from Mesopotamia. Archeologists rediscovered the game in the Royal Cemetery at Ur, in modern-day southern Iraq, during excavations between 1922 and 1934. The rules were reconstructed from the game board and a Babylonian clay tablet, and they’re similar to Parcheesi. You’ll need both luck and skill to win.
Two players each begin with seven tokens in their home, and the first player to move all seven to the goal is the winner. Players take turns throwing four dice. These dice are four-pointed pyramid shapes called tetrahedrons. Each die has two marked points, giving an even chance that the dice come up marked or unmarked. Instead of dice, our game uses coins whose heads act as the marked point. The player can move a token one space for each marked point that comes up. This means they can move a single token between zero and four spaces, though they’re most likely to roll two spaces.
The tokens travel along the path indicated in Figure 63-2. Only one token may exist on a space at a time. If a token lands on an opponent’s token while in the shared middle path, the opponent’s token is sent back home. If a token lands on the middle flower square, it is safe from being landed on. If a token lands on any of the other four flower tiles, the player gets to roll again. Our game will represent the tokens with the letters X and O.
A video featuring YouTuber Tom Scott and British Museum curator Irving Finkel discussing the Royal Game of Ur can be found at https://www.youtube.com/watch?v=WZskjLq040I.
royal_game_of_ur.py
1"""The Royal Game of Ur, by Al Sweigart al@inventwithpython.com
2A 5,000 year old board game from Mesopotamia. Two players knock each
3other back as they race for the goal.
4More info https://en.wikipedia.org/wiki/Royal_Game_of_Ur
5This code is available at https://nostarch.com/big-book-small-python-programming
6Tags: large, board game, game, two-player
7"""
8
9import random, sys
10
11X_PLAYER = 'X'
12O_PLAYER = 'O'
13EMPTY = ' '
14
15# Set up constants for the space labels:
16X_HOME = 'x_home'
17O_HOME = 'o_home'
18X_GOAL = 'x_goal'
19O_GOAL = 'o_goal'
20
21# The spaces in left to right, top to bottom order:
22ALL_SPACES = 'hgfetsijklmnopdcbarq'
23X_TRACK = 'HefghijklmnopstG' # (H stands for Home, G stands for Goal.)
24O_TRACK = 'HabcdijklmnopqrG'
25
26FLOWER_SPACES = ('h', 't', 'l', 'd', 'r')
27
28BOARD_TEMPLATE = """
29 {} {}
30 Home Goal
31 v ^
32+-----+-----+-----+--v--+ +--^--+-----+
33|*****| | | | |*****| |
34|* {} *< {} < {} < {} | |* {} *< {} |
35|****h| g| f| e| |****t| s|
36+--v--+-----+-----+-----+-----+-----+-----+--^--+
37| | | |*****| | | | |
38| {} > {} > {} >* {} *> {} > {} > {} > {} |
39| i| j| k|****l| m| n| o| p|
40+--^--+-----+-----+-----+-----+-----+-----+--v--+
41|*****| | | | |*****| |
42|* {} *< {} < {} < {} | |* {} *< {} |
43|****d| c| b| a| |****r| q|
44+-----+-----+-----+--^--+ +--v--+-----+
45 ^ v
46 Home Goal
47 {} {}
48"""
49
50
51def main():
52 print('''The Royal Game of Ur, by Al Sweigart
53
54This is a 5,000 year old game. Two players must move their tokens
55from their home to their goal. On your turn you flip four coins and can
56move one token a number of spaces equal to the heads you got.
57
58Ur is a racing game; the first player to move all seven of their tokens
59to their goal wins. To do this, tokens must travel from their home to
60their goal:
61
62 X Home X Goal
63 v ^
64+---+---+---+-v-+ +-^-+---+
65|v<<<<<<<<<<<<< | | ^<|<< |
66|v | | | | | | ^ |
67+v--+---+---+---+---+---+---+-^-+
68|>>>>>>>>>>>>>>>>>>>>>>>>>>>>>^ |
69|>>>>>>>>>>>>>>>>>>>>>>>>>>>>>v |
70+^--+---+---+---+---+---+---+-v-+
71|^ | | | | | | v |
72|^<<<<<<<<<<<<< | | v<<<< |
73+---+---+---+-^-+ +-v-+---+
74 ^ v
75 O Home O Goal
76
77If you land on an opponent's token in the middle track, it gets sent
78back home. The **flower** spaces let you take another turn. Tokens in
79the middle flower space are safe and cannot be landed on.''')
80 input('Press Enter to begin...')
81
82 gameBoard = getNewBoard()
83 turn = O_PLAYER
84 while True: # Main game loop.
85 # Set up some variables for this turn:
86 if turn == X_PLAYER:
87 opponent = O_PLAYER
88 home = X_HOME
89 track = X_TRACK
90 goal = X_GOAL
91 opponentHome = O_HOME
92 elif turn == O_PLAYER:
93 opponent = X_PLAYER
94 home = O_HOME
95 track = O_TRACK
96 goal = O_GOAL
97 opponentHome = X_HOME
98
99 displayBoard(gameBoard)
100
101 input('It is ' + turn + '\'s turn. Press Enter to flip...')
102
103 flipTally = 0
104 print('Flips: ', end='')
105 for i in range(4): # Flip 4 coins.
106 result = random.randint(0, 1)
107 if result == 0:
108 print('T', end='') # Tails.
109 else:
110 print('H', end='') # Heads.
111 if i != 3:
112 print('-', end='') # Print separator.
113 flipTally += result
114 print(' ', end='')
115
116 if flipTally == 0:
117 input('You lose a turn. Press Enter to continue...')
118 turn = opponent # Swap turns to the other player.
119 continue
120
121 # Ask the player for their move:
122 validMoves = getValidMoves(gameBoard, turn, flipTally)
123
124 if validMoves == []:
125 print('There are no possible moves, so you lose a turn.')
126 input('Press Enter to continue...')
127 turn = opponent # Swap turns to the other player.
128 continue
129
130 while True:
131 print('Select move', flipTally, 'spaces: ', end='')
132 print(' '.join(validMoves) + ' quit')
133 move = input('> ').lower()
134
135 if move == 'quit':
136 print('Thanks for playing!')
137 sys.exit()
138 if move in validMoves:
139 break # Exit the loop when a valid move is selected.
140
141 print('That is not a valid move.')
142
143 # Perform the selected move on the board:
144 if move == 'home':
145 # Subtract tokens at home if moving from home:
146 gameBoard[home] -= 1
147 nextTrackSpaceIndex = flipTally
148 else:
149 gameBoard[move] = EMPTY # Set the "from" space to empty.
150 nextTrackSpaceIndex = track.index(move) + flipTally
151
152 movingOntoGoal = nextTrackSpaceIndex == len(track) - 1
153 if movingOntoGoal:
154 gameBoard[goal] += 1
155 # Check if the player has won:
156 if gameBoard[goal] == 7:
157 displayBoard(gameBoard)
158 print(turn, 'has won the game!')
159 print('Thanks for playing!')
160 sys.exit()
161 else:
162 nextBoardSpace = track[nextTrackSpaceIndex]
163 # Check if the opponent has a tile there:
164 if gameBoard[nextBoardSpace] == opponent:
165 gameBoard[opponentHome] += 1
166
167 # Set the "to" space to the player's token:
168 gameBoard[nextBoardSpace] = turn
169
170 # Check if the player landed on a flower space and can go again:
171 if nextBoardSpace in FLOWER_SPACES:
172 print(turn, 'landed on a flower space and goes again.')
173 input('Press Enter to continue...')
174 else:
175 turn = opponent # Swap turns to the other player.
176
177def getNewBoard():
178 """
179 Returns a dictionary that represents the state of the board. The
180 keys are strings of the space labels, the values are X_PLAYER,
181 O_PLAYER, or EMPTY. There are also counters for how many tokens are
182 at the home and goal of both players.
183 """
184 board = {X_HOME: 7, X_GOAL: 0, O_HOME: 7, O_GOAL: 0}
185 # Set each space as empty to start:
186 for spaceLabel in ALL_SPACES:
187 board[spaceLabel] = EMPTY
188 return board
189
190
191def displayBoard(board):
192 """Display the board on the screen."""
193 # "Clear" the screen by printing many newlines, so the old
194 # board isn't visible anymore.
195 print('\n' * 60)
196
197 xHomeTokens = ('X' * board[X_HOME]).ljust(7, '.')
198 xGoalTokens = ('X' * board[X_GOAL]).ljust(7, '.')
199 oHomeTokens = ('O' * board[O_HOME]).ljust(7, '.')
200 oGoalTokens = ('O' * board[O_GOAL]).ljust(7, '.')
201
202 # Add the strings that should populate BOARD_TEMPLATE in order,
203 # going from left to right, top to bottom.
204 spaces = []
205 spaces.append(xHomeTokens)
206 spaces.append(xGoalTokens)
207 for spaceLabel in ALL_SPACES:
208 spaces.append(board[spaceLabel])
209 spaces.append(oHomeTokens)
210 spaces.append(oGoalTokens)
211
212 print(BOARD_TEMPLATE.format(*spaces))
213
214
215def getValidMoves(board, player, flipTally):
216 validMoves = [] # Contains the spaces with tokens that can move.
217 if player == X_PLAYER:
218 opponent = O_PLAYER
219 track = X_TRACK
220 home = X_HOME
221 elif player == O_PLAYER:
222 opponent = X_PLAYER
223 track = O_TRACK
224 home = O_HOME
225
226 # Check if the player can move a token from home:
227 if board[home] > 0 and board[track[flipTally]] == EMPTY:
228 validMoves.append('home')
229
230 # Check which spaces have a token the player can move:
231 for trackSpaceIndex, space in enumerate(track):
232 if space == 'H' or space == 'G' or board[space] != player:
233 continue
234 nextTrackSpaceIndex = trackSpaceIndex + flipTally
235 if nextTrackSpaceIndex >= len(track):
236 # You must flip an exact number of moves onto the goal,
237 # otherwise you can't move on the goal.
238 continue
239 else:
240 nextBoardSpaceKey = track[nextTrackSpaceIndex]
241 if nextBoardSpaceKey == 'G':
242 # This token can move off the board:
243 validMoves.append(space)
244 continue
245 if board[nextBoardSpaceKey] in (EMPTY, opponent):
246 # If the next space is the protected middle space, you
247 # can only move there if it is empty:
248 if nextBoardSpaceKey == 'l' and board['l'] == opponent:
249 continue # Skip this move, the space is protected.
250 validMoves.append(space)
251
252 return validMoves
253
254
255if __name__ == '__main__':
256 main()