-
Notifications
You must be signed in to change notification settings - Fork 0
Players and Multi Controller
Added in 0.5.0.
PlayerManager turns input sources into player seats: join-by-press,
disconnect handling, and reconnection that never reshuffles who is P1 and who
is P2.
from texastoast import HubPoller, KeyboardInput, PlayerManager
manager = PlayerManager(
max_players=2,
join_buttons=("a", "start"),
on_join=lambda p: banner(f"Player {p.index + 1} joined!"),
on_leave=lambda p: banner(f"Player {p.index + 1} disconnected"),
)
manager.add_source(keyboard) # the keyboard is a claimable seat too
for hub in hubs:
poller = HubPoller(hub).start()
game.on_close(poller.stop)
manager.add_hub(poller) # one seat candidate per controller
def update(dt):
manager.update() # join scan + hotplug watch, once per frame
for player in manager.joined_players:
state = player.poll() # a Player IS an InputSource
move(player.index, state.dx, state.dy, dt)Frame-driven, like the UI widgets: call manager.update() once per frame,
then poll the seats you care about.
Joining is edge-triggered: a seat is claimed by a fresh press of a join
button on an unassigned source. Holding A through the join screen claims one
seat, not one per frame. on_join(player) fires synchronously from
update(), on the game thread — no marshaling.
add_hub accepts a MagmaHub or a HubPoller (both expose
num_controllers) and registers a MagmaHubInput per controller. Seats can
be dropped manually with manager.release(player), which returns the source
to the claimable pool.
Connection state rides the duck-typed connected attribute —
MagmaHubInput forwards its hub's; KeyboardInput has none and therefore
never leaves.
When a controller vanishes (loose wire, unplugged hub):
- The seat goes inactive and
on_leave(player)fires. - The seat's
poll()returns idle — never the buttons that were held at the moment of disconnect. A stuck-walking player is the failure this exists to prevent. - The seat keeps its source. When the controller answers again, the same
seat reactivates and
on_join(player)fires again — a bounced cable is the same physical controller, and reshuffling P1/P2 mid-game is exactly what a living-room console must not do. (There is deliberately no separateon_rejoin.)
player.joined (has a source) and player.active (source currently
connected) are distinct for exactly this window.
The whole flow runs against SimBus, no
hardware needed — this is how tests/test_players.py covers it:
from texastoast import PlayerManager, simulated_hub
from texastoast.i2c.protocol import BTN_A
hub, sim = simulated_hub()
manager = PlayerManager()
manager.add_hub(hub)
sim.press(BTN_A); hub.poll(); manager.update() # joins seat 0
sim.disconnect_hub(0x08); hub.poll(); manager.update() # leave, idle poll
sim.reconnect_hub(0x08); hub.poll(); manager.update() # same seat, rejoinedexamples/two_player_demo.py plays this out live: keyboard P1, a simulated
autopilot P2 that joins, walks, gets "unplugged" mid-stride, and comes back —
to the same seat.
texastoast · PyPI · Apache-2.0