Mancala

Tags: large, boardgame, game, twoplayer

The board game Mancala is “seed-sowing” game in which two players select pockets of seeds to spread across the other pockets on the board while trying to collect as many in their store as possible. There are several variants of this game across different cultures. The name comes from the Arab word naqala, meaning “to move.”

To play, grab the seeds from a pit on your side of the board and place one in each subsequent pit, going counterclockwise and skipping your opponent’s store. If your last seed lands in an empty pit of yours, move the opposite pit’s seeds into that pit. If the last placed seed is in your store, you get a free turn.

The game ends when all of one player’s pits are empty. The other player claims the remaining seeds for their store, and the winner is the one with the most seeds. More information about Mancala and its variants can be found at https://en.wikipedia.org/wiki/Mancala.

mancala.py
  1"""Mancala, by Al Sweigart al@inventwithpython.com
  2The ancient seed-sowing game.
  3This code is available at https://nostarch.com/big-book-small-python-programming
  4Tags: large, board game, game, two-player"""
  5
  6import sys
  7
  8# A tuple of the player's pits:
  9PLAYER_1_PITS = ('A', 'B', 'C', 'D', 'E', 'F')
 10PLAYER_2_PITS = ('G', 'H', 'I', 'J', 'K', 'L')
 11
 12# A dictionary whose keys are pits and values are opposite pit:
 13OPPOSITE_PIT = {'A': 'G', 'B': 'H', 'C': 'I', 'D': 'J', 'E': 'K',
 14                   'F': 'L', 'G': 'A', 'H': 'B', 'I': 'C', 'J': 'D',
 15                   'K': 'E', 'L': 'F'}
 16
 17# A dictionary whose keys are pits and values are the next pit in order:
 18NEXT_PIT = {'A': 'B', 'B': 'C', 'C': 'D', 'D': 'E', 'E': 'F', 'F': '1',
 19            '1': 'L', 'L': 'K', 'K': 'J', 'J': 'I', 'I': 'H', 'H': 'G',
 20            'G': '2', '2': 'A'}
 21
 22# Every pit label, in counterclockwise order starting with A:
 23PIT_LABELS = 'ABCDEF1LKJIHG2'
 24
 25# How many seeds are in each pit at the start of a new game:
 26STARTING_NUMBER_OF_SEEDS = 4  # (!) Try changing this to 1 or 10.
 27
 28
 29def main():
 30    print('''Mancala, by Al Sweigart al@inventwithpython.com
 31
 32The ancient two-player, seed-sowing game. Grab the seeds from a pit on
 33your side and place one in each following pit, going counterclockwise
 34and skipping your opponent's store. If your last seed lands in an empty
 35pit of yours, move the opposite pit's seeds into your store. The
 36goal is to get the most seeds in your store on the side of the board.
 37If the last placed seed is in your store, you get a free turn.
 38
 39The game ends when all of one player's pits are empty. The other player
 40claims the remaining seeds for their store, and the winner is the one
 41with the most seeds.
 42
 43More info at https://en.wikipedia.org/wiki/Mancala
 44''')
 45    input('Press Enter to begin...')
 46
 47    gameBoard = getNewBoard()
 48    playerTurn = '1'  # Player 1 goes first.
 49
 50    while True:  # Run a player's turn.
 51        # "Clear" the screen by printing many newlines, so the old
 52        # board isn't visible anymore.
 53        print('\n' * 60)
 54        # Display board and get the player's move:
 55        displayBoard(gameBoard)
 56        playerMove = askForPlayerMove(playerTurn, gameBoard)
 57
 58        # Carry out the player's move:
 59        playerTurn = makeMove(gameBoard, playerTurn, playerMove)
 60
 61        # Check if the game ended and a player has won:
 62        winner = checkForWinner(gameBoard)
 63        if winner == '1' or winner == '2':
 64            displayBoard(gameBoard)  # Display the board one last time.
 65            print('Player ' + winner + ' has won!')
 66            sys.exit()
 67        elif winner == 'tie':
 68            displayBoard(gameBoard)  # Display the board one last time.
 69            print('There is a tie!')
 70            sys.exit()
 71
 72
 73def getNewBoard():
 74    """Return a dictionary representing a Mancala board in the starting
 75    state: 4 seeds in each pit and 0 in the stores."""
 76
 77    # Syntactic sugar - Use a shorter variable name:
 78    s = STARTING_NUMBER_OF_SEEDS
 79
 80    # Create the data structure for the board, with 0 seeds in the
 81    # stores and the starting number of seeds in the pits:
 82    return {'1': 0, '2': 0, 'A': s, 'B': s, 'C': s, 'D': s, 'E': s,
 83            'F': s, 'G': s, 'H': s, 'I': s, 'J': s, 'K': s, 'L': s}
 84
 85
 86def displayBoard(board):
 87    """Displays the game board as ASCII-art based on the board
 88    dictionary."""
 89
 90    seedAmounts = []
 91    # This 'GHIJKL21ABCDEF' string is the order of the pits left to
 92    # right and top to bottom:
 93    for pit in 'GHIJKL21ABCDEF':
 94        numSeedsInThisPit = str(board[pit]).rjust(2)
 95        seedAmounts.append(numSeedsInThisPit)
 96
 97    print("""
 98+------+------+--<<<<<-Player 2----+------+------+------+
 992      |G     |H     |I     |J     |K     |L     |      1
100       |  {}  |  {}  |  {}  |  {}  |  {}  |  {}  |
101S      |      |      |      |      |      |      |      S
102T  {}  +------+------+------+------+------+------+  {}  T
103O      |A     |B     |C     |D     |E     |F     |      O
104R      |  {}  |  {}  |  {}  |  {}  |  {}  |  {}  |      R
105E      |      |      |      |      |      |      |      E
106+------+------+------+-Player 1->>>>>-----+------+------+
107
108""".format(*seedAmounts))
109
110
111def askForPlayerMove(playerTurn, board):
112    """Asks the player which pit on their side of the board they
113    select to sow seeds from. Returns the uppercase letter label of the
114    selected pit as a string."""
115
116    while True:  # Keep asking the player until they enter a valid move.
117        # Ask the player to select a pit on their side:
118        if playerTurn == '1':
119            print('Player 1, choose move: A-F (or QUIT)')
120        elif playerTurn == '2':
121            print('Player 2, choose move: G-L (or QUIT)')
122        response = input('> ').upper().strip()
123
124        # Check if the player wants to quit:
125        if response == 'QUIT':
126            print('Thanks for playing!')
127            sys.exit()
128
129        # Make sure it is a valid pit to select:
130        if (playerTurn == '1' and response not in PLAYER_1_PITS) or (
131            playerTurn == '2' and response not in PLAYER_2_PITS
132        ):
133            print('Please pick a letter on your side of the board.')
134            continue  # Ask player again for their move.
135        if board.get(response) == 0:
136            print('Please pick a non-empty pit.')
137            continue  # Ask player again for their move.
138        return response
139
140
141def makeMove(board, playerTurn, pit):
142    """Modify the board data structure so that the player 1 or 2 in
143    turn selected pit as their pit to sow seeds from. Returns either
144    '1' or '2' for whose turn it is next."""
145
146    seedsToSow = board[pit]  # Get number of seeds from selected pit.
147    board[pit] = 0  # Empty out the selected pit.
148
149    while seedsToSow > 0:  # Continue sowing until we have no more seeds.
150        pit = NEXT_PIT[pit]  # Move on to the next pit.
151        if (playerTurn == '1' and pit == '2') or (
152            playerTurn == '2' and pit == '1'
153        ):
154            continue  # Skip opponent's store.
155        board[pit] += 1
156        seedsToSow -= 1
157
158    # If the last seed went into the player's store, they go again.
159    if (pit == playerTurn == '1') or (pit == playerTurn == '2'):
160        # The last seed landed in the player's store; take another turn.
161        return playerTurn
162
163    # Check if last seed was in an empty pit; take opposite pit's seeds.
164    if playerTurn == '1' and pit in PLAYER_1_PITS and board[pit] == 1:
165        oppositePit = OPPOSITE_PIT[pit]
166        board['1'] += board[oppositePit]
167        board[oppositePit] = 0
168    elif playerTurn == '2' and pit in PLAYER_2_PITS and board[pit] == 1:
169        oppositePit = OPPOSITE_PIT[pit]
170        board['2'] += board[oppositePit]
171        board[oppositePit] = 0
172
173    # Return the other player as the next player:
174    if playerTurn == '1':
175        return '2'
176    elif playerTurn == '2':
177        return '1'
178
179
180def checkForWinner(board):
181    """Looks at board and returns either '1' or '2' if there is a
182    winner or 'tie' or 'no winner' if there isn't. The game ends when a
183    player's pits are all empty; the other player claims the remaining
184    seeds for their store. The winner is whoever has the most seeds."""
185
186    player1Total = board['A'] + board['B'] + board['C']
187    player1Total += board['D'] + board['E'] + board['F']
188    player2Total = board['G'] + board['H'] + board['I']
189    player2Total += board['J'] + board['K'] + board['L']
190
191    if player1Total == 0:
192        # Player 2 gets all the remaining seeds on their side:
193        board['2'] += player2Total
194        for pit in PLAYER_2_PITS:
195            board[pit] = 0  # Set all pits to 0.
196    elif player2Total == 0:
197        # Player 1 gets all the remaining seeds on their side:
198        board['1'] += player1Total
199        for pit in PLAYER_1_PITS:
200            board[pit] = 0  # Set all pits to 0.
201    else:
202        return 'no winner'  # No one has won yet.
203
204    # Game is over, find player with largest score.
205    if board['1'] > board['2']:
206        return '1'
207    elif board['2'] > board['1']:
208        return '2'
209    else:
210        return 'tie'
211
212
213# If the program is run (instead of imported), run the game:
214if __name__ == '__main__':
215    main()

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