turn-based
Import from @pierre/ecs/modules/turn-based.
Turn order for turn-based games. This is an opt-in turn-based module, not a core ECS primitive — real-time games don’t use it.
Two layers, use whichever fits your state:
TurnOrder— plain data: a list of seats, the active one, and counters. Use it when your game state is plain data (a JSON object you save, diff or send over the wire), for hot-seat board and card games.TurnCycler— the same rotation over entities in anEcsWorld, driven by tags.
Plain turn order
Section titled “Plain turn order”import type { TurnOrder } from '@pierre/ecs/modules/turn-based';
import { addSeat, advanceTurn, currentSeat, makeTurnOrder, passTurnTo, removeSeat, reverseTurns } from '@pierre/ecs/modules/turn-based';
interface Game { turn: TurnOrder<string> }
const game: Game = { turn: makeTurnOrder(['ann', 'bob', 'cy']) };
advanceTurn(game.turn); // bob's turnadvanceTurn(game.turn, 1); // a "skip" card: cy is passed over, so ann is nextconsole.log(currentSeat(game.turn), game.turn.rounds); // 'ann' 1
reverseTurns(game.turn); // a "reverse" card: play goes the other waypassTurnTo(game.turn, 'cy'); // cy won the trick and leadsremoveSeat(game.turn, 'bob'); // bob drops outaddSeat(game.turn, 'dee'); // dee joins at the end
const saved = JSON.stringify(game); // still plain datainterface TurnOrder<T> { seats: T[]; // play order; any id: number, string, EntityId active: number; // index into seats of the seat holding the turn direction: 1 | -1; // which way advanceTurn walks turns: number; // completed turns rounds: number; // completed rounds (passes over the wrap point)}
function makeTurnOrder<T>(seats: readonly T[], first?: number): TurnOrder<T>;function currentSeat<T>(order: TurnOrder<T>): T | undefined;function nextSeat<T>(order: TurnOrder<T>): T | undefined;function advanceTurn<T>(order: TurnOrder<T>, skip?: number): boolean;function passTurnTo<T>(order: TurnOrder<T>, seat: T): void;function reverseTurns<T>(order: TurnOrder<T>): void;function addSeat<T>(order: TurnOrder<T>, seat: T, index?: number): void;function removeSeat<T>(order: TurnOrder<T>, seat: T): boolean;makeTurnOrdercopiesseatsand makesfirst(an index, default0) active. It throws whenfirstis not a valid index of a non-empty list.currentSeat/nextSeatread the active seat and the one after it in the current direction. Both areundefinedwith no seats.advanceTurnends the turn and moves to the next seat indirection, passing overskipseats.turnsgoes up by one; each pass over the wrap point (last to first going forward, first to last when reversed) adds one torounds, and a largeskipcan cross it several times. It returns whether it wrapped at least once. With one seat every advance wraps; with no seats it does nothing and returnsfalse.passTurnTohands the turn to a given seat, as when the winner of a trick leads. It counts a turn but not a round, and throws, changing nothing, when the seat is not in the order.reverseTurnsflipsdirection; the active seat keeps the turn.addSeat/removeSeatchange the seat list without disturbing whose turn it is. If the removed seat held the turn, the seat that would have come next indirectiontakes it. Counters are untouched.removeSeatreturnsfalsefor an absent seat.- An extra turn (a Checkers multi-jump) needs no helper: do not advance.
- Seats are expected to be distinct; lookups match the first
===hit.
World: TurnCycler
Section titled “World: TurnCycler”Round-robin active-turn rotation across tagged entities, built on the same
advanceTurn.
interface TurnCyclerTags { readonly controlled: TagDef; // entities that take turns readonly activeTurn: TagDef; // entity currently holding the turn readonly cameraTarget?: TagDef; // optional — moved in lockstep with activeTurn}
class TurnCycler { constructor(world: EcsWorld, tags: TurnCyclerTags);
readonly activeEntityId: EntityId | undefined; readonly allControlledEntitiesActed: boolean;
advance(): boolean;}Parameterized by the controlled tag (which entities take turns) and the
activeTurn tag (which one currently holds the turn). Optional cameraTarget
is moved in lockstep with activeTurn when provided.
advance()moves theactiveTurntag to the nextcontrolledentity in insertion order, and movescameraTargetin lockstep when configured.- Returns
truewhen the round wrapped (i.e. AI turn should run next),falseotherwise. No-ops returntruewhen there are 0 or 1 controlled entities. allControlledEntitiesActedistruewhen the active-turn tag is back on the first controlled entity — i.e. every controlled entity has had its turn this round. Triviallytruefor 0 or 1 controlled entities.
import { TurnCycler } from '@pierre/ecs/modules/turn-based';
const cycler = new TurnCycler(world, { controlled: ControlledTag, activeTurn: ActiveTurnTag, cameraTarget: CameraTargetTag, // optional});
// after each player actionconst wrapped = cycler.advance();if (wrapped) runAiTurn();Import via @pierre/ecs/modules/turn-based.