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