Skip to content

Tutorial Part 1 Text Catan Game

Josef Waller edited this page Mar 13, 2018 · 1 revision

In this tutorial, we are going to make a text interface for playing Catan with PyCatan

To start, download board_renderer.py from the examples folder. We'll eventually make our own, but for now let's just get the game up and running. Now let's create a new file called text_game.py and add some skeleton comments:

from pycatan.game import Game
from board_renderer import BoardRenderer

if __name__ == "__main__":
    # Starting phase
    # Twice for each player
        # Place a free settlement
        # Place a free road connected to that settlement

    # While the game is not won
        # Roll the die
        # Hand out appropriate resources
        # Let this turn's player trade
        # Let this turn's player build

    # Announce winner
    pass

Run python3 text_game.py to make sure the file runs. Now let's create a game of Catan.

if __name__ == "__main__":
    # Create a new game of Catan
    game = Game()

Now let's make BoardRenderer use this game's board.

# Set up board to render
br = BoardRenderer(board=game.board, center=[50, 10])
# Draw the board
br.render()

Now running python3 text_game.py should show the board, in ascii form. Let's add the starting phase, but first let's quickly add a small helper method for reading integers from the user's input.

# Get an integer from standard input and return it
# If a non-integer value is entered, continue to prompt the user
def integer_input(str_prompt):
    while True:
        try:
            return int(input(str_prompt))
        except ValueError:
            print("Please enter a valid integer")
            continue

Now let's add the starting phase of the game. Currently, we'll just add settlements.

# Starting phase
# Twice for each player
for p in game.players + list(reversed(game.players)):
    # Render the board
    br.render()
    # Place a free settlement
    with terminal.location(0, terminal.height - 1):
        # Prompt the player for input
        print("Player %s, it is your turn" % p.num)
        while True:
            # Get the row
            row = integer_input("Enter the row you want to place the settlement: ")
            # Get the index
            index = integer_input("Enter the index you want to place your settlement: ")
            # Try to build the settlement
            res = game.add_settlement(player=p.num, r=row, i=index, is_starting=True)
            # If invalid, just repeat
            if res != Statuses.ALL_GOOD:
                continue

Now running the code should allow users to place their starting settlements. Let's add roads:

# If invalid, just repeat
if res != Statuses.ALL_GOOD:
    continue
# Place a free road connected to that settlement
points = game.board.points[row][index].connected_points
# Render the board
br.render()
# Loop while road input is invalid
while True:
    with terminal.location(0, terminal.height - 1):
        print("Please choose from one of the following points to build your road to:")
        # Go through valid points
        for i in range(len(points)):
            # Print each point as an option
            print("%s. %s" % (i, points[i].position))
        # Prompt user to chose a point
        choice = integer_input("Which point do you want to build your road to? ")
        # Ensure choice is valid
        if choice < 0 or choice > len(points) - 1:
            print("Please choose a valid option")
        else:
            # Add the road
            res = game.add_road(player=p.num, start=[row, index], end=points[choice].position, is_starting=True)
            # If successful, break the loop and go to the next player
            if res == Statuses.ALL_GOOD:
                break
# Break from outer loop, so that the next player is prompted:w
break

Great, now players should be able to add their settlements and roads at the beginning of the game. However, if they enter an invalid position for a settlement, the game doesn't tell them why it's invalid. PyCatan will return several different Statuses for different reasons why a placement is invalid. Let's use these to tell the player why their settlement position is invalid:

# Try to build the settlement
res = game.add_settlement(player=p.num, r=row, i=index, is_starting=True)
# If invalid, tell the player why and allow them to re-enter a position
if res != Statuses.ALL_GOOD:
    if res == Statuses.ERR_BLOCKED:
        print("Another settlement is too close for you to build a settlement there!")
    elif res == Statuses.ERR_BAD_POINT:
        print("The point %s is not on the board!" % [row, index])
    else:
        # These are the only 2 errors that should run during the building phase, so something went terribly wrong
        print("An internal error occured. res = %s." % res)
    # Repeat the loop to allow the user to reenter the position
    continue
# Code here will execute if the settlement position is correct

Great. Now let's add road building right after, assuming the player entered their settlement correctly

# Code here will execute if the settlement position is correct
# Place a free road connected to that settlement
points = game.board.points[row][index].connected_points
# Render the board
br.render()
# Loop while road input is invalid
while True:
    with terminal.location(0, terminal.height - 1):
        print("Please choose from one of the following points to build your road to:")
        # Go through valid points
        for i in range(len(points)):
            # Print each point as an option
            print("%s. %s" % (i, points[i].position))
        # Prompt user to chose a point
        choice = integer_input("Which point do you want to build your road to? ")
        # Ensure choice is valid
        if choice < 0 or choice > len(points) - 1:
            print("Please choose a valid option")
        else:
            # Add the road
            res = game.add_road(player=p.num, start=[row, index], end=points[choice].position, is_starting=True)
            # If successful, break the loop and go to the next player
            if res == Statuses.ALL_GOOD:
                break
# Break from outer loop, so that the next player is prompted
break

Clone this wiki locally