Text game Tutorial

In this tutorial, we’re going to use pycatan to make a version of the game playable in the terminal. The finished text game is available here: https://gist.github.com/josefwaller/a3c3c19b19e46150224e7a4f34bc4dbd

Initializing the Game

Our game is just going to use the beginner board, which is already provided.

from pycatan import Game
from pycatan.board import BeginnerBoard

game = Game(BeginnerBoard())

Done! Now we have a 4 player game of catan all set up. We can show the board by simply printing it.

print(game.board)

Part 1: The Building Phase

First let’s set up the turn order:

# Building phase
player_order = list(range(len(game.players)))
for i in player_order + list(reversed(player_order)):
    current_player = game.players[i]
    print("Player %d, it is your turn!" % (i + 1))
    coords = choose_intersection(game.board.get_valid_settlement_coords(current_player, ensure_connected = False))

Now we need the player to choose where they want to place their first settlement. Usually the player could just click on it, but since we’re in the console it’s a big harder. Luckily BoardRenderer lets us label the different intersections, and Board has methods to filter out the valid ones:

# We're going to do some more complicated rendering, so import the BoardRenderer utility class
from pycatan.board import BoardRenderer
import string

renderer = BoardRenderer(game.board)

# Long list of all letters and numbers to use as labels over the points
label_letters = string.ascii_lowercase + string.ascii_uppercase + "123456789"
def choose_intersection(intersection_coords, renderer):
    # Now we make a map of letter to intersection, and give that to board renderer
    intersection_list = [game.board.intersections[i] for i in intersection_coords]
    intersection_labels = {intersection_list[i]: label_letters[i] for i in range(len(intersection_list))}
    renderer.render_board(intersection_labels = intersection_labels)

Run it and we get:

      3:1         2:1
       u--g--P--b--f--R--Z
       | 10  |  2  |  9  | 2:1
    t--e--c--d--X--G--E--m--F
2:1 | 12  |  6  |  4  | 10  |
 a--Q--W--C--Y--l--B--j--O--r--2
 |  9  | 11  |   R |  3  |  8  | 3:1
 z--D--k--x--y--L--s--1--N--T--A
2:1 |  8  |  3  |  4  |  5  |
    i--I--p--q--M--V--w--K--n
       |  5  |  6  | 11  | 2:1
       J--U--S--H--o--v--h
      3:1         3:1

Each intersection is now labelled with a letter! Let’s sort the points quickly so that the board looks nicer:

def get_coord_sort_by_xy(c):
    x, y = renderer.get_coords_as_xy(c)
    return 1000 * x + y


label_letters = string.ascii_lowercase + string.ascii_uppercase + "123456789"
def choose_intersection(intersection_coords, prompt):
    # Label all the letters on the board
    intersection_list = [game.board.intersections[i] for i in intersection_coords]
    intersection_list.sort(key = lambda i: get_coord_sort_by_xy(i.coords))
    intersection_labels = {intersection_list[i]: label_letters[i] for i in range(len(intersection_list))}
    renderer.render_board(intersection_labels = intersection_labels)

Now we just ask the player where they want to build a settlement:

def choose_intersection(intersection_coords, prompt):
    ...
    # Prompt the user
    letter = input(prompt)
    letter_to_intersection = {v: k for k, v in intersection_labels.items()}
    intersection = letter_to_intersection[letter]
    return intersection.coords

And then finally, build a settlement there!

game.build_settlement(player = current_player, coords = coords, cost_resources = False, ensure_connected = False)

The entire file at this point should look like this:

from pycatan import Game
from pycatan.board import BeginnerBoard, BoardRenderer
import string

game = Game(BeginnerBoard())
renderer = BoardRenderer(game.board)

def get_coord_sort_by_xy(c):
    x, y = renderer.get_coords_as_xy(c)
    return 1000 * x + y


label_letters = string.ascii_lowercase + string.ascii_uppercase + "123456789"
def choose_intersection(intersection_coords, prompt):
    # Label all the letters on the board
    intersection_list = [game.board.intersections[i] for i in intersection_coords]
    intersection_list.sort(key = lambda i: get_coord_sort_by_xy(i.coords))
    intersection_labels = {intersection_list[i]: label_letters[i] for i in range(len(intersection_list))}
    renderer.render_board(intersection_labels = intersection_labels)
    # Prompt the user
    letter = input(prompt)
    letter_to_intersection = {v: k for k, v in intersection_labels.items()}
    intersection = letter_to_intersection[letter]
    return intersection.coords

player_order = list(range(len(game.players)))
for i in player_order + list(reversed(player_order)):
    current_player = game.players[i]
    print("Player %d, it is your turn!" % (i + 1))
    coords = choose_intersection(game.board.get_valid_settlement_coords(current_player, ensure_connected = False), "Where do you want to build your settlement? ")
    game.build_settlement(player = current_player, coords = coords, cost_resources = False, ensure_connected = False)

Now run the code, and try building a settlement on the intersection labelled M. It should look like this:

Player 1, it is your turn!




                 3:1         2:1
                  a--b--c--d--e--f--g
                  | 10  |  2  |  9  | 2:1
               h--i--j--k--l--m--n--o--p
           2:1 | 12  |  6  |  4  | 10  |
            q--r--s--t--u--v--w--x--y--z--A
            |  9  | 11  |   R |  3  |  8  | 3:1
            B--C--D--E--F--G--H--I--J--K--L
           2:1 |  8  |  3  |  4  |  5  |
               M--N--O--P--Q--R--S--T--U
                  |  5  |  6  | 11  | 2:1
                  V--W--X--Y--Z--1--2
                 3:1         3:1



Where do you want to build your settlement? Q
Player 2, it is your turn!




                 3:1         2:1
                  a--b--c--d--e--f--g
                  | 10  |  2  |  9  | 2:1
               h--i--j--k--l--m--n--o--p
           2:1 | 12  |  6  |  4  | 10  |
            q--r--s--t--u--v--w--x--y--z--A
            |  9  | 11  |   R |  3  |  8  | 3:1
            B--C--D--E--F--.--G--H--I--J--K
           2:1 |  8  |  3  |  4  |  5  |
               L--M--N--.--s--.--O--P--Q
                  |  5  |  6  | 11  | 2:1
                  R--S--T--U--V--W--X
                 3:1         3:1



Where do you want to build your settlement? C
Player 3, it is your turn!




                 3:1         2:1
                  a--b--c--d--e--f--g
                  | 10  |  2  |  9  | 2:1
               h--i--j--k--l--m--n--o--p
           2:1 | 12  |  6  |  4  | 10  |
            q--r--s--t--u--v--w--x--y--z--A
            |  9  | 11  |   R |  3  |  8  | 3:1
            '--s--'--B--C--.--D--E--F--G--H
           2:1 |  8  |  3  |  4  |  5  |
               '--I--J--.--s--.--K--L--M
                  |  5  |  6  | 11  | 2:1
                  N--O--P--Q--R--S--T
                 3:1         3:1

The players can now build settlements And notice that the next player doesn’t have the intersections directly beside it as an option to select - because they aren’t valid intersections for their settlement. Now let’s allow the player to build a road. First we’ll add another function that allows the player to choose a road from the board:

def choose_path(path_coords, prompt):
    # Label all the paths with a letter
    path_list = [game.board.paths[i] for i in path_coords]
    path_labels = {path_list[i]: label_letters[i] for i in range(len(path_coords))}
    renderer.render_board(path_labels = path_labels)
    # Ask the user for a letter
    letter = input(prompt)[0]
    # Get the path from the letter entered by the user
    letter_to_path = {v: k for k, v in path_labels.items()}
    return letter_to_path[letter].path_coords

And now use it in the building phase:

# Get the valid locations for the player to build a road
road_options = game.board.get_valid_road_coords(current_player, connected_intersection = coords)
# Ask the user to choose one
road_coords = choose_path(road_options, "Where do you want to build your road to? ")
# Build a road
game.build_road(player = current_player, path_coords = road_coords, cost_resources = False)

Now the player is able to build a road! The last thing to add to the building phase is the player getting the resources around a settlement when they build it. So let’s add that:

game.build_settlement(player = current_player, coords = coords, cost_resources = False, ensure_connected = False)
# Add the resources around the intersection to the player's hand
current_player.add_resources(game.board.get_hex_resources_for_intersection(coords))

The entire file should look like this now:

from pycatan import Game
from pycatan.board import BeginnerBoard, BoardRenderer
import string

game = Game(BeginnerBoard())
renderer = BoardRenderer(game.board)

def get_coord_sort_by_xy(c):
    x, y = renderer.get_coords_as_xy(c)
    return 1000 * x + y


label_letters = string.ascii_lowercase + string.ascii_uppercase + "123456789"
def choose_intersection(intersection_coords, prompt):
    # Label all the letters on the board
    intersection_list = [game.board.intersections[i] for i in intersection_coords]
    intersection_list.sort(key = lambda i: get_coord_sort_by_xy(i.coords))
    intersection_labels = {intersection_list[i]: label_letters[i] for i in range(len(intersection_list))}
    renderer.render_board(intersection_labels = intersection_labels)
    # Prompt the user
    letter = input(prompt)
    letter_to_intersection = {v: k for k, v in intersection_labels.items()}
    intersection = letter_to_intersection[letter]
    return intersection.coords

def choose_path(path_coords, prompt):
    # Label all the paths with a letter
    path_list = [game.board.paths[i] for i in path_coords]
    path_labels = {path_list[i]: label_letters[i] for i in range(len(path_coords))}
    renderer.render_board(path_labels = path_labels)
    # Ask the user for a letter
    letter = input(prompt)[0]
    # Get the path from the letter entered by the user
    letter_to_path = {v: k for k, v in path_labels.items()}
    return letter_to_path[letter].path_coords

player_order = list(range(len(game.players)))
for i in player_order + list(reversed(player_order)):
    current_player = game.players[i]
    print("Player %d, it is your turn!" % (i + 1))
    coords = choose_intersection(game.board.get_valid_settlement_coords(current_player, ensure_connected = False), "Where do you want to build your settlement? ")
    game.build_settlement(player = current_player, coords = coords, cost_resources = False, ensure_connected = False)
    current_player.add_resources(game.board.get_hex_resources_for_intersection(coords))
    # Print the road options
    road_options = game.board.get_valid_road_coords(current_player, connected_intersection = coords)
    road_coords = choose_path(road_options, "Where do you want to build your road to? ")
    game.build_road(player = current_player, path_coords = road_coords, cost_resources = False)

Part 2: The game loop

Let’s implement the actual game phase now. It will look something like:

# Roll the dice
# Distribute resources
# Allow the player to perform actions

The first 2 are very easy to do:

import random

...

current_player_num = 0
while True:
    current_player = game.players[current_player_num]
    print("Player %d, it is your turn now" % (current_player_num + 1))
    # Roll the dice
    dice = random.randint(1, 6) + random.randint(1, 6)
    print("Player %d rolled a %d" % (current_player_num + 1, dice))
    if dice == 7:
        # TBA
        pass
    else:
        game.add_yield_for_roll(dice)

Now let’s show the player what resources they have. Each player’s resources is available as a dict of resource -> amount:

print("Player %d, you have these resources:" % (current_player_num + 1))
for res, amount in current_player.resources:
    print("    %s: %d" % (res, amount))

It’ll look something like this:

Player 1, you have these resources:
    Lumber: 0
    Brick: 0
    Wool: 1
    Grain: 1
    Ore: 0

Now in the final part of the game loop setup, let’s have the player choose what they want to do. We’ll put it in a loop so they can choose as many things as they want:

choice = 0
while choice != 4:
    # Print the player's resources
    print("Player %d, you have these resources:" % (current_player_num + 1))
    for res, amount in current_player.resources:
        print("%s: %d" % (res, amount))
    # Prompt the player for an action
    print("Choose what to do:")
    print("1 - Build something")
    print("2 - Trade")
    print("3 - Play a dev card")
    choice = int(input("->  "))
    if choice == 1:
        pass
    elif choice == 2:
        pass
    elif choice == 3:
        pass
    current_player_num = (current_player_num + 1) % len(game.players)

Perfect! In the next part we’ll implement the building choice that will allow the player to build new settlements, roads and cities.

Part 3: Building new buildings

Now it’s easy to implement 1. Let’s start with roads

# We'll use this to determine if the player has enough resources to build a road
from pycatan.board import BuildingType

...

if choice == 1:
    print("What do you want to build? ")
    print("1 - Settlement")
    print("2 - City")
    print("3 - Road")
    building_choice = int(input("->  "))
    if building_choice == 1:
        pass
    elif building_choice == 2:
        pass
    elif building_choice == 3:
        # Check the player has enough resources
        if not current_player.has_resources(BuildingType.ROAD.get_required_resources()):
            print("You don't have enough resources to build a road")
            continue
        # Get the valid road coordinates
        valid_coords = game.board.get_valid_road_coords(current_player)
        # If there are none
        if not valid_coords:
            print("There are no valid places to build a road")
            continue
        # Have the player choose one
        path_coords = choose_path(valid_coords, "Where do you want to build a road?")
        game.build_road(current_player, path_coords)

Building settlements and cities is very similar:

 elif building_choice == 2:
    # Check the player has enough resources
    if not current_player.has_resources(BuildingType.CITY.get_required_resources()):
        print("You don't have enough resources to build a city")
        continue
    # Get the valid coords to build a city at
    valid_coords = game.board.get_valid_city_coords(current_player)
    # Have the player choose one
    coords = choose_intersection(valid_coords, "Where do you want to build a city?  ")
    # Build the city
    game.upgrade_settlement_to_city(current_player, coords)
elif building_choice == 3:
    # Check the player has enough resources
    if not current_player.has_resources(BuildingType.ROAD.get_required_resources()):
        print("You don't have enough resources to build a road")
        continue
    # Get the valid road coordinates
    valid_coords = game.board.get_valid_road_coords(current_player)
    # If there are none
    if not valid_coords:
        print("There are no valid places to build a road")
        continue
    # Have the player choose one
    path_coords = choose_path(valid_coords, "Where do you want to build a road?")
    game.build_road(current_player, path_coords)

And voila! The players can now build roads, settlements and cities!

Part 4: Trading in resources

Let’s implement the second choise now, allowing the player to trade in 4:1 resources (or 2:1 if they are connected to a harbor). PyCatan provides the very useful get_possible_trades() method:

elif choice == 2:
    possible_trades = list(current_player.get_possible_trades())
    print("Choose a trade: ")
    for i in range(len(possible_trades)):
        print("%d:" % i)
        for res, amount in possible_trades[i].items():
            print("    %s: %d" % (res, amount))

The player should see something like this, assuming they have 4 grain:

Choose a trade:
0:
    Grain: -4
    Lumber: 1
1:
    Grain: -4
    Wool: 1
2:
    Grain: -4
    Brick: 1
3:
    Grain: -4
    Ore: 1

Now we simply have them choose one:

trade_choice = int(input('->  '))
trade = possible_trades[trade_choice]
current_player.add_resources(trade)

And now the player has the option to trade in 4:1 and 2:1 resources! PyCatan handles checking which harbors they’re connected to and which resources they have 4 of.

Part 5: Development Cards

Let’s go back to the building section of our game and add the ability to build a development card:

from pycatan import Game, DevelopmentCard

...

if choice == 1:
    print("What do you want to build? ")
    print("1 - Settlement")
    print("2 - City")
    print("3 - Road")
    print("4 - Development Card")

    building_choice = int(input('->  '))

    ...

    elif building_choice == 4:
        # Check the player has the resources to build a development card
        if not current_player.has_resources(DevelopmentCard.get_required_resources()):
            print("You do not have the resources to build a development card")
            continue
        # Build a card and tell the player what they build
        dev_card = game.build_development_card(current_player)
        print("You built a %s card" % dev_card)

Now the player can build a development card! Let’s list the development cards the player has with the resources:

print("and you have these development cards")
for dev_card, amount in current_player.development_cards.items():
    print("    %s: %d" % (dev_card, amount))

Perfect! Now for the hard part - implementing these development cards. PyCatan doesn’t implement playing development cards, but provides many utility methods that makes implementing them easier. First let’s have the player choose a dev card to play:

elif choice == 3:
    # Choose a development card
    print("What card do you want to play?")
    dev_cards = [card for card, amount in current_player.development_cards.items() if amount > 0 and card is not DevelopmentCard.VICTORY_POINT]
    for i in range(len(dev_cards)):
        print("%d: %s" % (i, dev_cards[i]))
    card_to_play = dev_cards[int(input('->  '))]

Now let’s implement each of the development card types:

# We'll implement this later
def move_robber(player):
    pass

...

# Doesn't actually do anything but remove the card from the player's hand and recalculate largest army
game.play_development_card(current_player, card_to_play)

if card_to_play is DevelopmentCard.KNIGHT:
    move_robber(current_player)

elif card_to_play is DevelopmentCard.YEAR_OF_PLENTY:
    # Have the player choose 2 resources to receive
    for _ in range(2):
        resource = choose_resource("What resource do you want to receive?")
        # Add that resource to the player's hand
        current_player.add_resources({resource: 1})

elif card_to_play is DevelopmentCard.ROAD_BUILDING:
    # Allow the player to build 2 roads
    for _ in range(2):
        valid_path_coords = game.board.get_valid_road_coords(current_player)
        path_coords = choose_path(valid_path_coords, "Choose where to build a road: ")
        game.build_road(current_player, path_coords, cost_resources = False)

elif card_to_play is DevelopmentCard.MONOPOLY:
    # Choose a resource
    resource = choose_resource("What resource do you want to take?")
    # Remove that resource from everyone else's hands and add it to the current player's hand
    for i in range(len(game.players)):
        player = game.players[i]
        if player is not current_player:
            amount = player.resources[resource]
            player.remove_resources({ resource: amount })
            current_player.add_resources({ resource: amount })
            print("Took %d from player %d" % (amount, i + 1))

And done! The player can now play any development card they want, except knights, which we’ll do next.

Part 6: Moving the Robber

When moving the robber, we need to do 2 things 1. Move the robber to where the player says 2. Take a random resource from a player on the hex

Let’s do 1 first:

def choose_hex(hex_coords, prompt):
    # Label all the hexes with a letter
    hex_list = [game.board.hexes[i] for i in hex_coords]
    hex_list.sort(key = lambda h: get_coord_sort_by_xy(h.coords))
    hex_labels = {hex_list[i]: label_letters[i] for i in range(len(hex_list))}
    renderer.render_board(hex_labels = hex_labels)
    letter = input(prompt)
    letter_to_hex = {v: k for k, v in hex_labels.items()}
    return letter_to_hex[letter].coords

...

def move_robber(player):
    # Don't let the player move the robber back onto the same hex
    hex_coords = choose_hex([c for c in game.board.hexes if c != game.board.robber], "Where do you want to move the robber? ")
    game.board.robber = hex_coords

Now the player can move the knight to any hex they choose! Now let’s steal a card from someone:

# Choose a player to steal a card from
potential_players = list(game.board.get_players_on_hex(hex_coords))
print("Choose who you want to steal from:")
for p in potential_players:
    i = game.players.index(p)
    print("%d: Player %d" % (i + 1, i + 1))
p = int(input('->  ')) - 1
# If they try and steal from another player they lose their chance to steal
to_steal_from = game.players[p] if game.players[p] in potential_players else None
if to_steal_from:
    resource = to_steal_from.get_random_resource()
    player.add_resources({ resource: 1})
    to_steal_from.remove_resources({ resource: 1 })
    print("Stole 1 %s for player %d" % (resource, p + 1))

Perfect! Now the player can steal a random card form the player of their choice. Now back at the top of our game loop:

   if dice == 7:
       move_robber(current_player)

We're not going to implement checking for which players have over 7 resource, since it's difficult to let the players choose what cardsto get rid of through just text format.
But it would be something like this: ::

   for i in range(len(game.players)):
       total_resources = sum[amount for res, amount in game.players[i].resources]
       if total_resources > 7:
           print("You need to lose %d resources" % (total_resources - 7))
           # Fancy-dancy UI stuff to let the player select 7 resources

We’re almost done our text game! The last part is victory points!

Part 7: Victory Points

Now let’s show the victory points at the top:

   while choice != 4:
       print(game.board)
       print("Current Victory point standings:")
       for i in range(len(game.players)):
           print("Player %d: %d VP" % (i + 1, game.get_victory_points(game.players[i])))

And now let's end the game at 10 victory points: ::

    if game.get_victory_points(current_player) >= 10:
       print("Congratuations! Player %d wins!" % (current_player_num + 1))
       print("Final board:")
       print(game.board)
       sys.exit(0)

And we’re done! Let’s add some more info on where those VPs are coming from:

print("Current Victory point standings:")
for i in range(len(game.players)):
    print("Player %d: %d VP" % (i + 1, game.get_victory_points(game.players[i])))
print("Current longest road owner: %s" % ("Player %d" % (game.players.index(game.longest_road_owner) + 1) if game.longest_road_owner else "Nobody"))
print("Current largest army owner: %s" % ("Player %d" % (game.players.index(game.largest_army_owner) + 1) if game.largest_army_owner else "Nobody"))

And with that, our game is completely finished! The full text game source code is available on github: https://gist.github.com/josefwaller/a3c3c19b19e46150224e7a4f34bc4dbd