← Game Event Script

GES CARD LAB · PLAYABLE EXAMPLES

Card games. Your rules.

Choose a game to play. Open the editor to change its rules. Edits are autosaved to Draft; Save keeps a separate copy in Saved. Read and change the GES rules, then try your own variations.

First load: about 67 MB. Runs locally; your code is saved in this browser. View the source.

Game rules · GES

Loading saved code …

Loading compiler and syntax highlighting …

Game Notice

The game could not continue

Check the error and adjust the rules if needed. Choose “New game” to restart.


    

Card environment reference

Write rules with on Event(…), send commands with emit Message(label: value), and read the board with :board.function(label: value). Parameters below are listed in their required order.

Zones

Signature / valueMeaning
setup: [table: […], players: […], actions: […]]:board.create receives the complete board. Each player: [id: player, zones: […]]. Include every selected player exactly once. Optional actions contains global action maps.
playerLayout: #aroundTable / #bottomOptional field in the board setup. Default #aroundTable seats players around the table. #bottom places all players side by side below it, upright and in player order. This changes presentation only, not turn order.
position: #nw, #n, #ne, #w, #center, #e, #sw, #s, #seTable zones only; default #center. Aliases: #left = #w, #right = #e, #top = #n, #bottom = #s. Aliases share rows; extensions return compass tags. Each position has its own rows; zones in each row are centered together in definition order.
position: #left / #center / #rightPlayer zones: align the whole row. Initially #left; omitted values inherit the current alignment. Change alignment on a new row. Left player rotates +90°, right −90°; top and bottom stay upright.
newRow: true / falseOptional, defaults to false. Starts a new row within the player area or the selected table position. The first zone starts row zero, even with true. No nested rows or align fields.
id: Tag · label: TextRequired, globally unique zone tag and display label. All zone references (zone, source, destination) use tags. Player IDs and card IDs remain numbers.
layout: #pile / #spreadDefault #pile. #spread is available on the table and in player rows. Combine with #public to display every card.
visibility: #hidden / #owner / #top / #publicCount only / owner sees cards / top card / all cards. Defaults to #public on the table, #owner for players.
cards: […]Initial property maps; defaults to []. Use deck()[:shuffle] to shuffle in GES.

Events received by GES

Signature / valueMeaning
on PrepareGame(players)Create zones and decks. Emit custom messages to deal cards. All queued preparation messages finish before the first round and turn start automatically with player 0.
on BeginRound(number, players)Before the first turn of each round. Numbered from 1, anchored at the initial player; skipped seats count. Optional hook.
on EndRound(number, players)At the round boundary. A GES handler must request NextRound; without one the next round starts automatically. May offer actions with finishTurn: false for human draws. The selected next starting player acts during the pause. Also called before EndGame; check :board.ending() before offering actions.
on BeginTurn(player)Activate actions with Action, skip directly with NextPlayersTurn(), or request EndGame().
YourHandler(action, player[, card])Your registered handler receives the numeric activation ID and player, plus a selected card for card actions. Complete or reject exactly once.
on EndTurn(player)Apply effects before automatic advancement to the next player.
on EndGame(players)Determine the winners and emit Finish(winners: […]).

Messages emitted by GES

Signature / valueMeaning
Action(action, label, optional, finishTurn, consumable, handler, cards / zone / area)Activate a tag unique for this player during BeginTurn, a waiting EndRound, or after successful completion. label is Text. Handler(action, player) for buttons; Handler(action, player, card) with a final cards list for card choices. area: #table or a numeric player ID places a button at that area’s heading. Alternatively, zone: Tag makes an existing zone clickable with a green outline instead of showing a button.
consumable: #auto / 1–1000 / #manual / #neverConsume after one success, after N successes, explicitly via ConsumeAction, or never. Rejections do not count. #never requires optional: true and finishTurn: false.
optional / finishTurnNon-optional actions must be fully consumed before turn end. finishTurn requests turn end on full consumption, waiting for other required actions if necessary.
ConsumeAction(action)Consume the current #manual activation by its numeric ID, after completing it successfully.
Action(spec: […])Action map with priority (default 0) and optional player. Use exactly one Text caption: label or button. button places a centered button below the zone cards (zone) or each target card (cards); for area it stays in the heading. Button offers are excluded from card-click choices. Multiple buttons sit together and wrap. Explicit player queues for that player's next turn; otherwise the action lasts this turn. Defaults: optional true, finishTurn false, consumable #auto.
ActionGroup(spec: […])Fields: group, actions, optional player, priority, optional, exclusive, data. Alternatives inherit recipient and priority. One completed alternative completes the group. exclusive (default true) locks after first successful execution. Mandatory groups default optional false; alternatives omit optional/priority/player.
priority: 10Open mandatory offers block player actions with lower priority. Optional offers never block. Same-priority alternatives remain selectable; global actions stay accessible. Nothing executes automatically.
:board.actions(player)Active, queued and consumed actions/groups with stable ref, id, kind, player, priority, optional, queued, active, consumed. Actions include group and remaining; groups include exclusive and data. Pass filtered entries to ClearActions.
SetActionCards(action, cards)Refresh an active card action by tag, without resetting its consumption counter.
Complete(action)Accept the pending request after the rule has applied its effects.
Reject(action, reason)Reject with a Text reason; retain the current offers.
NextRound()Continue from EndRound or an accepted between-round action once required actions are consumed. Ignored during game shutdown.
ClearActions() / ClearActions(actions)Remove all current-player offers, including queued groups, or selected entries from :board.actions(player). Group removal includes its alternatives; an empty group is removed. Global offers and other players remain when no list is supplied.
NextPlayersTurn()Request normal turn advancement in BeginTurn, after acceptance, or in on EndTurn(player). Required actions must be consumed; finishTurn can request the transition automatically.
NextPlayersTurn(nextPlayer: X)Select a valid next player in BeginTurn, after acceptance or in on EndTurn(player). Forward seat distance counts toward round completion. Last NextPlayersTurn command wins.
NextPlayersTurn(repeatTurnForPlayer: true)Repeat the active player after a full circuit; false means normal advancement. Available in BeginTurn, after acceptance or in on EndTurn(player).
EndGame()Request EndGame(players) from a round hook, BeginTurn, an answered action, or EndTurn. Closes the open round before EndGame(players).
Finish(winners)During EndGame: a list of distinct player IDs wins; [] means nobody wins.
NoticeTable(_ text)Replace the centered status text at the bottom of the table. Persists until changed; empty Text clears it.
NoticeTable(_ text, pushOld)With true, save the current text and show the new text. Up to 64 saved texts; false only replaces the visible text.
NoticeTable(pop)With true, restore the most recently saved text. False or an empty stack does nothing.
NoticeTable(_ text, stackClear)With true, clear saved texts and show the new text. New game always resets the stack and visible text. Plain NoticeTable preserves the stack.
ZoneBadge(_ text, zone[, color])Badge beside an existing zone's label; zone is its Tag ID. Same colors and lifetime as PlayerBadge. Empty text removes it. Example: emit ZoneBadge('Dealer', zone: #dealer, color: #blue).
TableBadge(_ text[, color])Badge beside the Table heading. Same colors and lifetime as PlayerBadge. Empty text removes it. Example: emit TableBadge('Round 2', color: #yellow).
PlayerBadge(_ text, player[, color])Set a free-text badge beside a player’s name; empty text removes it. Badges persist until changed or New game. Optional color tag: #green (default), #red, #yellow, #blue or #gray. Example: emit PlayerBadge('Winner', player: 0, color: #green). Omitting color resets it to green.
Notice(_ text, title: Text)Show a modal dialog after the rule cascade. Text is unlabeled; title is optional: emit Notice('Hello', title: 'Rules'). Without title, the heading is Game Notice. Multiple notices display one after another; Complete does not erase them.

Board extensions

Mutations run immediately; later queries in the same handler see the result. Previously read values remain immutable snapshots. Complete(action) is still required. Operations validate before changing the board, but a later error or Reject does not undo earlier mutations. Card movement and reverse are available during preparation, round hooks, actions and EndTurn. State writes also work in BeginTurn and EndGame.

Signature / valueMeaning
:board.create(setup)Once during preparation. Validates and creates the entire board atomically. Maximum 64 zones, 512 cards and 16 rows per player or table position. Unknown fields and duplicate IDs fail setup.
:board.take(source, destination, count)Move count top cards during preparation, round hooks, actions or EndTurn. Never refills or shuffles; does not complete an action.
:board.setstate(player, key, value)Player-specific information such as #skip; nothing deletes. Read with :board.state(player, key).
:board.move(card, source, destination)Move an explicit card and return its snapshot. Does not complete an action.
:board.draw(source, destination)Move the top card and return its snapshot, or nothing if empty. Does not complete an action; GES owns recycling.
:board.reverse()Toggle +1/-1 seat order during preparation, round hooks, actions or EndTurn. Round progress is preserved. Use before an explicit next-player selection.
:board.setstate(key, value)Store rule data under a Tag key (maximum 64). nothing deletes the key. Read via :board.state(key: …); absent values are nothing.
:board.setorder(zone, cards)Apply an exact permutation of a zone's card IDs. GES can use [:shuffle] to produce the order.
:board.movecards(source, destination, cards)Move a list of distinct source card IDs in supplied order. Same phases as :board.take; does not complete an action. Use [:shuffle] in GES to choose random order. Last card becomes the top.
:board.cards(zone)List of card snapshots [id, properties], from bottom to top.
:board.card(id)Card snapshot or nothing.
:board.top(zone)Top card snapshot or nothing.
:board.state(key)Immutable rule value or nothing. Key is a Tag; use is nothing to check absence.
:board.direction()Current direction: +1 or -1.
:board.ending()True after EndGame is requested, including the final EndRound hook.
:board.roundnumber()Current round number; 0 during preparation.
:board.current()Current player ID, or nothing before selection.
:board.players()List of player IDs in seat order.
:board.playercount()Number of players (1–4).
:board.next(player)Next player, accounting for already processed skip commands.
:board.table()Table zone snapshots in creation order: [id, label, position, row, layout, count]. Position and layout are tags. Player zones have position: nothing and a numeric row index.
:board.zones(player)That player’s zone snapshots, with the same fields.
:board.lastaction()Last accepted action kind as Text; initially empty.

Examples

DealCards and BeginGame are custom messages defined by your game, not environment events. Send it after creating zones and decks in PrepareGame(players):

// At the end of PrepareGame(players):
// emit DealCards(players: players)

on DealCards(players) {
    for amount in [3, 2, 3] {
        for player in players {
            :board.take(source: #draw,
                destination: hand(player: player), count: amount)
        }
    }
}

// Mau Mau: use [1, 1, 1, 1, 1] instead.
function hand(player) be ('hand' + (player as :Text)) as :Tag

function board(players) be [
    table: [
        [id: #draw, label: 'Draw pile', visibility: #hidden,
            cards: deck()[:shuffle]],
        [id: #discard, label: 'Discard pile', visibility: #top],
        [id: #reserve, label: 'Reserve', newRow: true]
    ],
    players: players[:select player => [id: player, zones: [
        [id: hand(player: player), label: 'Hand', position: #center, layout: #spread]
    ]]]
]

// In PrepareGame(players), before emitting DealCards:
:board.create(setup: board(players: players))

// In BeginTurn(player):
emit Action(action: #draw, label: 'Draw', optional: true,
    finishTurn: false, consumable: #auto, handler: Draw(action, player), zone: #draw)

emit Action(action: #play, label: 'Play', optional: true,
    finishTurn: true, consumable: #auto,
    handler: Play(action, player, card), cards: [12, 15])

emit Action(action: #pass, label: 'Pass', optional: true,
    finishTurn: true, consumable: #auto,
    handler: Pass(action, player), area: player)

// In a handler with consumable: #manual:
emit Complete(action: action)
emit ConsumeAction(action: action)

let top be :board.top(zone: #discard)

Supply initial cards directly when creating a zone. Cards may contain custom properties; the browser uses suit and rank for their faces. Action names are tags, labels are Text, and callbacks are Handler values. Card actions share one consumption counter across their selectable cards.

Global actions

Add an optional actions: […] list to :board.create. Each map uses the same fields as Action, with exactly one area, zone or cards target. Its handler takes action, or action, card, without player. Complete or Reject normally. Globals are available to any player and retain counters across turns and rounds; consumed globals stay consumed. Tags must be unique across global and player actions. finishTurn affects the active player's turn.

actions: [
    [action: #rules, label: 'Rules', optional: true, finishTurn: false,
        consumable: #never, handler: ShowRules(action), area: #table]
]

on ShowRules(action) {
    emit Complete(action: action)
    emit Notice('The rules of this game …')
}

Execution rules

After accepting an action, its consumption counter advances. You may activate further actions or update card choices. finishTurn ends the turn upon consumption; EndGame() requests game completion. Turn offers expire at turn end; completed queued groups and all their alternatives are removed, while unfinished queued offers retain their counters. The environment advances turns automatically. NextPlayersTurn variants control that advancement; they do not recursively emit on EndTurn(player).

Emits are queued: a query in the same handler cannot see their effects yet. Extensions can inspect all players; visibility filters the browser view only. Runtime errors stop the game. “Check code” checks compilation, not native message signatures.