Source code for simvx.core.input.players

"""Who an action belongs to, when more than one person is playing.

Routing decides which tree an input reaches; ownership decides which player it
belongs to, and the two are orthogonal. **A player is an input identity**: the
devices paired to it, and its own action state and edges.

```python
Input.players                          # mapping; player 0 is the default
two = Input.add_player()
two.pair(Input.controllers[1])            # join takes a pad out of the unpaired pool
node.input_player = two                # Input queries in this subtree answer for two
```

**Device state is never per player.** ``Input.is_key_pressed`` is a fact about
the keyboard, and stays one whoever is holding what. Only what resolves from a
binding is owned: the action queries, the action edges, and the action events
delivered to handlers.

**Player 0 is the default and owns every unpaired device**, so a single-player
game never sees this dimension: one implicit player owns everything, every
query resolves to it, and the fast path through the resolver is the one that
was there before players existed.

The join and leave flow, split-screen viewports and per-player UI focus are
deliberately not built. This is the seam they will sit on.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from ..math.types import Vec2
from .devices import ControllerDevice
from .enums import InputMethod

if TYPE_CHECKING:
    from .state import _Input

__all__ = ["InputPlayer"]

#: What a player can be paired to: one pad, or the desk as a whole. The desk is
#: spelled with the input method because that is already the engine's name for
#: "the keyboard and the mouse together", and a player holds it or does not.
type PairableDevice = ControllerDevice | InputMethod

#: The default player's id. It exists for the life of an ``Input`` and cannot
#: be removed, because something has to own a device nobody claimed.
DEFAULT_PLAYER = 0


[docs] class InputPlayer: """One player's input identity. Read them through ``Input.players``. A game with one player never touches this: ``Input.pressed("jump")`` answers for the default player, which owns everything. A co-op game pairs a pad to each player and sets ``input_player`` on each player's subtree, and the same call inside that subtree answers for that player. """ __slots__ = ("_id", "_state", "_pads", "_desk") def __init__(self, state: _Input, player_id: int) -> None: self._state = state self._id = player_id self._pads: set[int] = set() self._desk = False
[docs] def __repr__(self) -> str: return f"InputPlayer(id={self._id}, devices={sorted(self._pads)}, desk={self._desk})"
[docs] @property def id(self) -> int: """The player's id, which is the key it has in ``Input.players``.""" return self._id
[docs] @property def is_default(self) -> bool: """Whether this is player 0, which owns every device nobody else claimed.""" return self._id == DEFAULT_PLAYER
[docs] @property def devices(self) -> tuple[PairableDevice, ...]: """What is paired to this player, explicitly. Empty for the default player of a game that paired nothing, which still owns every device: what it owns is everything unclaimed rather than a list, so there is nothing to enumerate. """ pads = self._state._devices.objects found: list[PairableDevice] = [pads[pad_id] for pad_id in sorted(self._pads) if pad_id in pads] if self._desk: found.append(InputMethod.KEYBOARD_MOUSE) return tuple(found)
[docs] def pair(self, device: PairableDevice) -> None: """Take *device* for this player, out of the unpaired pool. Args: device: A :class:`~.devices.ControllerDevice` from ``Input.controllers``, or ``InputMethod.KEYBOARD_MOUSE`` for the desk. A device may be paired to more than one player, which is how one keyboard is split between two: the pairing says who hears it and the bindings say what each of them hears. """ pad_id, desk = _device_key(device) if desk: self._desk = True else: self._pads.add(pad_id) self._state._repair_ownership()
[docs] def unpair(self, device: PairableDevice) -> None: """Give *device* back to the unpaired pool. Unpairing what was never paired does nothing.""" pad_id, desk = _device_key(device) if desk: self._desk = False else: self._pads.discard(pad_id) self._state._repair_ownership()
[docs] def owns(self, device: PairableDevice) -> bool: """Whether input from *device* belongs to this player. True for anything explicitly paired, and for the default player also for anything no other player claimed. """ pad_id, desk = _device_key(device) if desk: return self._owns_desk() return self._owns_pad(pad_id)
# -- Action queries, scoped to this player --------------------------- # # The same four questions ``Input`` answers, asked about one player # explicitly. ``Input.pressed(...)`` is the ambient spelling and resolves # to whichever player the calling subtree belongs to; this is what a lobby # or a scoreboard outside any subtree uses, and it reads exactly as # ``Input.controllers[id].pressed(...)`` does for a pad.
[docs] def pressed(self, action: str) -> bool: """Is *action* open for this player?""" with self._scope(): return self._state.pressed(action)
[docs] def just_pressed(self, action: str) -> bool: """Did *action* open for this player during this frame?""" with self._scope(): return self._state.just_pressed(action)
[docs] def just_released(self, action: str) -> bool: """Did *action* close for this player during this frame?""" with self._scope(): return self._state.just_released(action)
[docs] def value(self, action: str) -> bool | float | Vec2: """What *action* reads for this player, in the type its bindings gave it.""" with self._scope(): return self._state.value(action)
[docs] def get_action_strength(self, action: str) -> float: """How hard this player is pushing *action*, 0.0 to 1.0.""" with self._scope(): return self._state.get_action_strength(action)
# -- Internals ------------------------------------------------------- def _scope(self): return self._state._as_player(self._id) def _owns_desk(self) -> bool: if self._desk: return True return self.is_default and not self._state._paired_desk def _owns_pad(self, pad_id: int) -> bool: if pad_id in self._pads: return True return self.is_default and pad_id not in self._state._paired_pads
def _device_key(device: PairableDevice) -> tuple[int, bool]: """A device as ``(pad id, is the desk)``, refusing anything that is neither.""" if isinstance(device, ControllerDevice): return device.id, False if device is InputMethod.KEYBOARD_MOUSE: return -1, True raise TypeError( f"{device!r} is not a device a player can be paired to. Pass a ControllerDevice from " "Input.controllers, or InputMethod.KEYBOARD_MOUSE for the keyboard and mouse." )