Royal Game of Ur

Tags: large, boardgame, game, twoplayer

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()

https://inventwithpython.com/bigbookpython/project63.html