Skip to content
magmacrunchmedia edited this page Aug 24, 2026 · 3 revisions

Input

Every input source in texastoast has the same two methods, poll() and is_pressed(), so a game can swap keyboard for hardware without changing.

InputState

poll() returns an InputState — eight booleans and two derived axes:

state = keyboard.poll()

state.up, state.down, state.left, state.right
state.a, state.b, state.start, state.select

state.dx    # -1.0, 0.0 or 1.0
state.dy    # -1.0, 0.0 or 1.0
state.is_any_direction()

Opposite directions cancel: holding left and right gives dx == 0.0.

dx/dy are raw axis reads and are not normalized — a diagonal has magnitude 1.41, not 1. Entity.move normalizes for you. If you integrate position yourself, do it too:

import math

dx, dy = state.dx, state.dy
mag = math.hypot(dx, dy)
if mag > 1.0:
    dx, dy = dx / mag, dy / mag
x += dx * speed * dt
y += dy * speed * dt

KeyboardInput

from texastoast import KeyboardInput

keyboard = KeyboardInput(game.root)
game.on_close(keyboard.destroy)   # release the bindings on exit
Button Keys
up / down / left / right Arrow keys, WASD
a Z, Enter
b X, Backspace
start Escape, P
select Shift

KeyboardInput binds on construction and tracks state until told otherwise. It is a state source, not an event source: poll() tells you what is held right now. For one-shot actions — confirming a menu, advancing dialogue — bind a key event instead, because polling will fire on every frame the key is down:

def on_key(event):
    if event.keysym in ("z", "Return"):
        dialogue.dismiss()

game.bind_key("<Key>", on_key)

destroy() removes every binding and clears the state. It is idempotent.

CompositeInput

Prefers hardware when it is actually responding, and falls back to the keyboard otherwise:

from texastoast.input.magma_hub import CompositeInput, MagmaHubInput

controls = CompositeInput(keyboard, MagmaHubInput(hub))
state = controls.poll()

controls.active_source   # "magma_hub", "keyboard" or "none"

Either argument may be None. With neither, poll() returns an idle state rather than raising.

The fallback is decided per call by hub.connected, so unplugging the hardware returns control to the keyboard on the next poll — provided something is polling the hub, since connected reflects the last poll's outcome. A HubPoller keeps that true without the game having to poll on its own thread.

In 0.1.x a hub on a dead or mock bus reported itself connected, so CompositeInput latched onto it and keyboard input stopped working altogether. See Magma Hub and I2C.

Recording and replay

Added in 0.4.0. InputRecorder wraps any source and writes every change to a .ttrec file; ReplayInput plays one back as a source in its own right.

from texastoast import InputRecorder, ReplayInput

recorder = InputRecorder(controls, "session.ttrec")
recorder.start()
game.on_close(recorder.stop)

replay = ReplayInput("session.ttrec")
replay.advance(dt)      # deterministic manual clock, for tests
replay.poll()           # -> InputState

Because the recorder is transparent — it delegates poll() and is_pressed() to the source it wraps — you can leave it in the chain and the game behaves exactly as before.

See Hardware Dev Kit for the file format and for replaying a session through the full I2C stack.

Writing your own source

Anything with poll() and is_pressed() works — there is no base class to inherit. InputSource in texastoast.input.abstract is a typing.Protocol you can use for type checking:

from texastoast import InputState

class AutopilotInput:
    """Walks right for two seconds, then stops."""

    def __init__(self):
        self._t = 0.0
        self._state = InputState()

    def tick(self, dt):
        self._t += dt
        self._state = InputState(right=self._t < 2.0)

    def poll(self):
        return self._state

    def is_pressed(self, button):
        return getattr(self._state, button, False)

Two sources ship with the engine and are worth reaching for before writing your own: ReplayInput above, and MagmaHubInput wrapping a HubPoller.

Clone this wiki locally