Part 1 - Moving a player around the screen¶
Note
This tutorial is still a work-in-progress. The resources being used are tracked here. Feel free to discuss this tutorial or share your progress on the Github Discussions forum.
In part 1 you will become familiar with the initialization, rendering, and event system of tcod. This will be done as a series of small implementations. It is recommend to save your progress after each section is finished and tested.
Initial script¶
First start with a modern top-level script.
You should have main.py
script from Part 0 - Setting up a project:
from __future__ import annotations
def main() -> None:
...
if __name__ == "__main__":
main()
You will replace body of the main
function in the following section.
Loading a tileset and opening a window¶
From here it is time to setup a tcod
program.
Download Alloy_curses_12x12.png [1] and place this file in your projects data/
directory.
This tileset is from the Dwarf Fortress tileset repository.
These kinds of tilesets are always loaded with columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437
.
Use the string "data/Alloy_curses_12x12.png"
to refer to the path of the tileset. [2]
Load the tileset with tcod.tileset.load_tilesheet
.
Pass the tileset to tcod.tileset.procedural_block_elements
which will fill in most Block Elements missing from Code Page 437.
Then pass the tileset to tcod.context.new
, you only need to provide the tileset
parameter.
tcod.context.new
returns a Context
which will be used with Python’s with
statement.
We want to keep the name of the context, so use the syntax: with tcod.context.new(tileset=tileset) as context:
.
The new block can’t be empty, so add pass
to the with statement body.
These functions are part of modules which have not been imported yet, so new imports for tcod.context
and tcod.tileset
must be added to the top of the script.
from __future__ import annotations
import tcod.context # Add these imports
import tcod.tileset
def main() -> None:
"""Load a tileset and open a window using it, this window will immediately close."""
tileset = tcod.tileset.load_tilesheet(
"data/Alloy_curses_12x12.png", columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437
)
tcod.tileset.procedural_block_elements(tileset=tileset)
with tcod.context.new(tileset=tileset) as context:
pass # The window will stay open for the duration of this block
if __name__ == "__main__":
main()
If an import fails that means you do not have tcod
installed on the Python environment you just used to run the script.
If you use an IDE then make sure the Python environment it is using is correct and then run pip install tcod
from the shell terminal within that IDE.
There is no game loop, so if you run this script now then a window will open and then immediately close. If that happens without seeing a traceback in your terminal then the script is correct.
Configuring an event loop¶
The next step is to keep the window open until the user closes it.
Since nothing is displayed yet a Console
should be created with "Hello World"
printed to it.
The size of the console can be used as a reference to create the context by adding the console to tcod.context.new
. [3]
Begin the main game loop with a while True:
statement.
To actually display the console to the window the Context.present
method must be called with the console as a parameter.
Do this first in the game loop before handing events.
Events are checked by iterating over all pending events with tcod.event.wait
.
Use the code for event in tcod.event.wait():
to begin handing events.
In the event loop start with the line print(event)
so that all events can be viewed from the program output.
Then test if an event is for closing the window with if isinstance(event, tcod.event.Quit):
.
If this is True then you should exit the function with raise SystemExit()
. [4]
from __future__ import annotations
import tcod.console
import tcod.context
import tcod.event
import tcod.tileset
def main() -> None:
"""Show "Hello World" until the window is closed."""
tileset = tcod.tileset.load_tilesheet(
"data/Alloy_curses_12x12.png", columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437
)
tcod.tileset.procedural_block_elements(tileset=tileset)
console = tcod.console.Console(80, 50)
console.print(0, 0, "Hello World") # Test text by printing "Hello World" to the console
with tcod.context.new(console=console, tileset=tileset) as context:
while True: # Main loop
context.present(console) # Render the console to the window and show it
for event in tcod.event.wait(): # Event loop, blocks until pending events exist
print(event)
if isinstance(event, tcod.event.Quit):
raise SystemExit()
if __name__ == "__main__":
main()
If you run this then you get a window saying "Hello World"
.
The window can be resized and the console will be stretched to fit the new resolution.
When you do anything such as press a key or interact with the window the event for that action will be printed to the program output.
An example game state¶
What exists now is not very interactive. The next step is to change state based on user input.
Like tcod
you’ll need to install attrs
with Pip, such as with pip install attrs
.
Start by adding an attrs
class called ExampleState
.
This a normal class with the @attrs.define()
decorator added.
This class should hold coordinates for the player.
It should also have a on_draw
method which takes tcod.console.Console
as a parameter and marks the player position on it.
The parameters for on_draw
are self
because this is an instance method and console: tcod.console.Console
.
on_draw
returns nothing, so be sure to add -> None
.
Console.print
is the simplest way to draw the player because other options would require bounds-checking.
Call this method using the players current coordinates and the "@"
character.
from __future__ import annotations
import attrs
import tcod.console
import tcod.context
import tcod.event
import tcod.tileset
@attrs.define()
class ExampleState:
"""Example state with a hard-coded player position."""
player_x: int
"""Player X position, left-most position is zero."""
player_y: int
"""Player Y position, top-most position is zero."""
def on_draw(self, console: tcod.console.Console) -> None:
"""Draw the player glyph."""
console.print(self.player_x, self.player_y, "@")
...
Now remove the console.print(0, 0, "Hello World")
line from main
.
Before the context is made create a new ExampleState
with player coordinates on the screen.
Each Console
has .width
and .height
attributes which you can divide by 2 to get a centered coordinate for the player.
Use Python’s floor division operator //
so that the resulting type is int
.
Modify the drawing routine so that the console is cleared, then passed to ExampleState.on_draw
, then passed to Context.present
.
...
def main() -> None:
"""Run ExampleState."""
tileset = tcod.tileset.load_tilesheet(
"data/Alloy_curses_12x12.png", columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437
)
tcod.tileset.procedural_block_elements(tileset=tileset)
console = tcod.console.Console(80, 50)
state = ExampleState(player_x=console.width // 2, player_y=console.height // 2)
with tcod.context.new(console=console, tileset=tileset) as context:
while True:
console.clear() # Clear the console before any drawing
state.on_draw(console) # Draw the current state
context.present(console) # Display the console on the window
for event in tcod.event.wait():
print(event)
if isinstance(event, tcod.event.Quit):
raise SystemExit()
if __name__ == "__main__":
main()
Now if you run the script you’ll see @
.
The next step is to move the player on events.
A new method will be added to the ExampleState
for this called on_event
.
on_event
takes a self
and a tcod.event.Event
parameter and returns nothing.
Events are best handled using Python’s Structural Pattern Matching. Consider reading Python’s Structural Pattern Matching Tutorial.
Begin matching with match event:
.
The equivalent to if isinstance(event, tcod.event.Quit):
is case tcod.event.Quit():
.
Keyboard keys can be checked with case tcod.event.KeyDown(sym=tcod.event.KeySym.LEFT):
.
Make a case for each arrow key: LEFT
RIGHT
UP
DOWN
and move the player in the direction of that key.
Since events are printed you can check the KeySym
of a key by pressing that key and looking at the printed output.
See KeySym
for a list of all keys.
Finally replace the event handling code in main
to defer to the states on_event
method.
The full script so far is:
from __future__ import annotations
import attrs
import tcod.console
import tcod.context
import tcod.event
import tcod.tileset
@attrs.define()
class ExampleState:
"""Example state with a hard-coded player position."""
player_x: int
"""Player X position, left-most position is zero."""
player_y: int
"""Player Y position, top-most position is zero."""
def on_draw(self, console: tcod.console.Console) -> None:
"""Draw the player glyph."""
console.print(self.player_x, self.player_y, "@")
def on_event(self, event: tcod.event.Event) -> None:
"""Move the player on events and handle exiting. Movement is hard-coded."""
match event:
case tcod.event.Quit():
raise SystemExit()
case tcod.event.KeyDown(sym=tcod.event.KeySym.LEFT):
self.player_x -= 1
case tcod.event.KeyDown(sym=tcod.event.KeySym.RIGHT):
self.player_x += 1
case tcod.event.KeyDown(sym=tcod.event.KeySym.UP):
self.player_y -= 1
case tcod.event.KeyDown(sym=tcod.event.KeySym.DOWN):
self.player_y += 1
def main() -> None:
"""Run ExampleState."""
tileset = tcod.tileset.load_tilesheet(
"data/Alloy_curses_12x12.png", columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437
)
tcod.tileset.procedural_block_elements(tileset=tileset)
console = tcod.console.Console(80, 50)
state = ExampleState(player_x=console.width // 2, player_y=console.height // 2)
with tcod.context.new(console=console, tileset=tileset) as context:
while True:
console.clear()
state.on_draw(console)
context.present(console)
for event in tcod.event.wait():
print(event)
state.on_event(event) # Pass events to the state
if __name__ == "__main__":
main()
Now when you run this script you have a player character you can move around with the arrow keys before closing the window.
You can review the part-1 source code here.
Footnotes