pile
Import from @pierre/ecs/modules/pile.
Ordered piles: a deck, a hand, a discard pile, a solitaire tableau column, a
bag of letter tiles. Index 0 is the bottom, the last index is the top.
Two layers, use whichever fits your state:
- Plain arrays —
moveTop,moveAllandmoveItemmove items between ordinary arrays of anything. Use them when your game state is plain data (a JSON object you save, diff or send over the wire) and has no world. - World piles — each pile is an entity and its members are other
entities, kept in step with a back-reference. Use them when the cards are
entities in an
EcsWorld.
Plain arrays
Section titled “Plain arrays”import { moveAll, moveItem, moveTop } from '@pierre/ecs/modules/pile';import { makeSeededRng, shuffle } from '@pierre/ecs/modules/rng';
interface Game { deck: number[]; discard: number[]; hand: number[] }
const game: Game = { deck: Array.from({ length: 10 }, (_, i) => i), discard: [], hand: [] };
shuffle(game.deck, makeSeededRng(7));moveTop(game.deck, game.hand, 5); // draw five
const played = game.hand[2]!;moveItem(game.hand, game.discard, played); // play a card from the middle of the hand
moveAll(game.hand, game.discard); // end of turn: discard the restconsole.log(game.deck.length, game.discard.length); // 5 5console.log(JSON.stringify(game)); // still plain datafunction moveTop<T>(from: T[], to: T[], count?: number): T[];function moveAll<T>(from: T[], to: T[], options?: MoveAllOptions): T[];function moveItem<T>(from: T[], to: T[], item: T, index?: number): boolean;
interface MoveAllOptions { reverse?: boolean }moveTop(from, to, count = 1)moves the topcountitems and keeps their order: a draw, or a run of cards moved as one. It moves fewer whenfromholds fewer, truncates a fractionalcountand treats a negative one as 0. It returns the moved items, bottom first.moveAll(from, to, { reverse })emptiesfromonto the top ofto.reverse: trueflips the order on the way, as when a face-up waste pile is turned back into the stock. It returns the items in the order they were added.moveItem(from, to, item, index?)moves one item, matched by===(the first match when it appears twice), to the top oftoor toindex. An out-of-rangeindexis clamped. It returnsfalse, changing neither array, whenitemis not infrom.- All three change the arrays in place. With
from === to,moveTopandmoveAllleave the array unchanged (moveAllwithreverseflips it), andmoveItemreorders it, countingindexafter the removal. - Shuffling is
shufflefrommodules/rng, which works on any array.
World piles
Section titled “World piles”Piles live in the world, so resetting it (world.clearAll()), saving it
(world.toJSON()) and loading it (world.loadJSON()) carry them along with no
extra code.
Example
Section titled “Example”import { EcsWorld } from '@pierre/ecs';import { addToPile, createPile, installPiles, movePileAll, movePileTop, pileItems, pileOf, pileSize, shufflePile } from '@pierre/ecs/modules/pile';import { makeSeededRng } from '@pierre/ecs/modules/rng';
const world = new EcsWorld();installPiles(world);
const cards = Array.from({ length: 10 }, () => world.createEntity());const deck = createPile(world, cards);const hand = createPile(world);const discard = createPile(world);
shufflePile(world, deck, makeSeededRng(7));movePileTop(world, deck, hand, 5); // draw five
const played = pileItems(world, hand)[2]!;addToPile(world, discard, played); // play a card from the middle of the handconsole.log(pileOf(world, played) === discard); // true
movePileAll(world, hand, discard); // end of turn: discard the restconsole.log(pileSize(world, deck), pileSize(world, discard)); // 5 5interface Pile { items: EntityId[] } // index 0 = bottominterface InPile { pile: EntityId }const PileDef: ComponentDef<Pile>; // on the pile entity, saved as 'pile'const InPileDef: ComponentDef<InPile>; // on each member, saved as 'inPile'
function installPiles(world: EcsWorld): void;
function createPile(world: EcsWorld, items?: readonly EntityId[]): EntityId;function pileItems(world: EcsWorld, pile: EntityId): readonly EntityId[];function pileSize(world: EcsWorld, pile: EntityId): number;function pileTop(world: EcsWorld, pile: EntityId): EntityId | undefined;function pileOf(world: EcsWorld, item: EntityId): EntityId | undefined;function indexInPile(world: EcsWorld, item: EntityId): number;
function addToPile(world: EcsWorld, pile: EntityId, item: EntityId, index?: number): void;function removeFromPile(world: EcsWorld, item: EntityId): boolean;function movePileTop(world: EcsWorld, from: EntityId, to: EntityId, count?: number): EntityId[];function movePileAll(world: EcsWorld, from: EntityId, to: EntityId, options?: MoveAllOptions): EntityId[];function shufflePile(world: EcsWorld, pile: EntityId, rand?: RandomFn): void;
interface MoveAllOptions { reverse?: boolean }installPilesregisters both components and must be called once per world, instead of registering them yourself.addToPileputs an item on top, or atindex. The item leaves the pile it was in first — the same pile included, which is how you reorder — soindexcounts positions after that removal. Out-of-range indices clamp.movePileTopmoves the topcountitems (default 1) and keeps their order: a draw, or a run of cards dragged as one. It moves fewer when the pile holds fewer, and returns what it moved, bottom first.movePileAllempties one pile onto another.reverse: trueflips the order on the way, as when a face-up waste pile is turned back into the stock.shufflePileshuffles in place; pass a seededrandfor a reproducible deal.- Passing an entity that is not a pile where a pile is expected throws.
One pile at a time
Section titled “One pile at a time”An entity is in at most one pile. Every operation keeps Pile.items and the
member’s InPile in step, so “what is in this pile?” and “which pile is this
in?” always agree.
pileItems returns the live array. Read it, iterate it, index it — but do not
push to it or splice it. A change that bypasses this module leaves InPile
out of step. For the same reason, do not set PileDef or InPileDef on an
entity yourself; use createPile and addToPile.
Destroying entities
Section titled “Destroying entities”installPiles subscribes to both component stores, so removal cleans up on
its own, immediately:
- Destroying a member (or deleting its
InPileDef) removes it from its pile. - Destroying a pile (or deleting its
PileDef) releases its members. They keep existing, in no pile.
Saving and loading
Section titled “Saving and loading”PileDef and InPileDef serialize with the rest of the world. Loading a save
restores the same entity ids, so the references stay valid. Merging a save into
a world that already uses those ids is not supported by the engine yet, for
piles or any other component that stores entity ids.
Not included
Section titled “Not included”- Card data. Face up or down, suit, cost — these are your components on the member entities.
- Layout. Where each member is drawn (a fanned column, a spread hand) is a
rule of your game, computed from
pileItemsandindexInPile. - Move rules. Whether a card may go onto a pile is your game’s check,
made before calling
addToPileormovePileTop.modules/drag-droptakes the same check as itsacceptscallback.
Dependencies
Section titled “Dependencies”modules/rng—shuffleandRandomFn, used byshufflePile.