@table-games-engine/game-kit 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ import { connectBridge, type StatePayload } from '@table-games-engine/game-kit/bridge';
2
+
3
+ const STRINGS = {
4
+ en: { you: 'You', opponent: 'Opponent', yourTurn: 'Your turn', waiting: 'Waiting for opponent…', won: 'You won!', lost: 'You lost.', cast: 'Cast Fire Bolt', rejected: 'That move is no longer legal.' },
5
+ } as const;
6
+ const locale = new URLSearchParams(location.search).get('locale') ?? 'en';
7
+ const t = STRINGS[locale as keyof typeof STRINGS] ?? STRINGS.en;
8
+ const app = document.getElementById('app')!;
9
+
10
+ const bridge = connectBridge(render, (kind) => {
11
+ if (kind === 'rejected') app.insertAdjacentHTML('beforeend', `<p>${t.rejected}</p>`);
12
+ });
13
+
14
+ function render(s: StatePayload): void {
15
+ const me = s.view.viewerId;
16
+ const rows = Object.values(s.view.players).map((p) => {
17
+ const life = (p as { resources?: Record<string, number> }).resources?.['life'] ?? '?';
18
+ return `<div class="row"><span>${p.playerId === me ? t.you : t.opponent}</span><span>${life} ♥</span></div>`;
19
+ });
20
+ const status =
21
+ s.status === 'finished'
22
+ ? (s.result?.winners.includes(me) ? t.won : t.lost)
23
+ : s.legalMoves.length > 0 ? t.yourTurn : t.waiting;
24
+ app.innerHTML = `${rows.join('')}<p>${status}</p>`;
25
+ for (const move of s.legalMoves) {
26
+ const button = document.createElement('button');
27
+ button.textContent = t.cast;
28
+ button.onclick = () => bridge.send(move);
29
+ app.appendChild(button);
30
+ }
31
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "./node_modules/@table-games-engine/game-kit/schema/game.schema.json",
3
+ "slug": "__SLUG__",
4
+ "name": "__NAME__",
5
+ "version": "0.1.0",
6
+ "engine": "^0.1.1",
7
+ "players": { "min": 2, "max": 2 },
8
+ "locales": ["en"],
9
+ "description": "Two wizards take turns casting Fire Bolt until one drops to 0 life.",
10
+ "rules": { "en": "rules.md" },
11
+ "tags": ["duel"],
12
+ "authors": ["replace-me"],
13
+ "credits": "",
14
+ "listed": false,
15
+ "client": { "kind": "iframe", "entry": "client/dist/index.html" },
16
+ "presets": [
17
+ { "id": "standard", "name": "Standard (5 life)", "config": { "life": 5 } },
18
+ { "id": "quick", "name": "Quick (3 life)", "config": { "life": 3 } }
19
+ ],
20
+ "simulation": { "maxSteps": 200 }
21
+ }
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ client/dist
3
+ .game-kit
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Template store game: a two-player Fire Bolt duel.
3
+ * Replace manifest, setup, handlers, win checker and legalMoves with your game.
4
+ */
5
+ import {
6
+ ok,
7
+ reject,
8
+ resolved,
9
+ type ActionHandler,
10
+ type ActionTypeId,
11
+ type GameId,
12
+ type GameManifest,
13
+ type GamePlugin,
14
+ type PhaseId,
15
+ type PlayerState,
16
+ type SeatIndex,
17
+ type SemanticVersion,
18
+ } from '@table-games-engine/core';
19
+
20
+ export const FIRE_BOLT = 'fire-bolt' as ActionTypeId;
21
+ const MAIN = 'main' as PhaseId;
22
+
23
+ const manifest = {
24
+ id: '__SLUG__',
25
+ name: '__NAME__',
26
+ version: '0.1.0',
27
+ minPlayers: 2,
28
+ maxPlayers: 2,
29
+ phases: {
30
+ entryPhase: MAIN,
31
+ phases: {
32
+ main: {
33
+ id: MAIN,
34
+ name: 'Main',
35
+ startingPriority: 'active-player',
36
+ legalActions: [FIRE_BOLT],
37
+ exitCondition: 'manual',
38
+ perPlayer: false,
39
+ onEnter: [],
40
+ onExit: [],
41
+ },
42
+ },
43
+ transitions: [],
44
+ },
45
+ actions: {
46
+ actions: {
47
+ [FIRE_BOLT]: {
48
+ id: FIRE_BOLT,
49
+ name: 'Fire Bolt',
50
+ speed: 'sorcery',
51
+ usesStack: true,
52
+ canTargetStack: false,
53
+ effects: [],
54
+ payloadSchema: { type: 'object', properties: {} },
55
+ },
56
+ },
57
+ },
58
+ triggers: { triggers: {} },
59
+ zones: { zones: {} },
60
+ winConditions: [],
61
+ configSchema: { type: 'object', properties: {} },
62
+ defaultConfig: {},
63
+ requiredMechanics: [],
64
+ } as unknown as GameManifest;
65
+
66
+ const fireBolt: ActionHandler = (ctx, frame) => {
67
+ const actor = 'actor' in frame ? frame.actor : frame.controller;
68
+ const target = ctx.getActivePlayers().find((p) => p !== actor);
69
+ // An illegal move: the server rejects the input and nothing changes.
70
+ if (!target) return reject('rule-violation');
71
+ return resolved([
72
+ { domain: 'player', operation: { type: 'add-resource', playerId: target, resource: 'life', delta: -2 } },
73
+ // Ending the turn is the handler's job: the engine then gives priority to the next player.
74
+ { domain: 'turn', operation: { type: 'advance' } },
75
+ ]);
76
+ };
77
+
78
+ const plugin: GamePlugin = {
79
+ id: '__SLUG__' as GameId,
80
+ version: '0.1.0' as SemanticVersion,
81
+ manifest,
82
+ requiredMechanics: [],
83
+ setup: (ctx, config, seats) => {
84
+ const life = typeof config['life'] === 'number' ? config['life'] : 5;
85
+ const players = Object.fromEntries(
86
+ seats.map((s) => [
87
+ s.playerId,
88
+ {
89
+ playerId: s.playerId,
90
+ seatIndex: s.seatIndex as SeatIndex,
91
+ status: 'active',
92
+ zones: {},
93
+ resources: { life },
94
+ counters: {},
95
+ flags: {},
96
+ custom: {},
97
+ } satisfies PlayerState,
98
+ ]),
99
+ );
100
+ // ctx.state is a valid empty state for this manifest (seats in turn order, first seat has priority); build on it.
101
+ return ok({ state: { ...ctx.state, players } });
102
+ },
103
+ actionHandlers: { [FIRE_BOLT]: fireBolt },
104
+ winConditionChecker: (ctx) => {
105
+ const fallen = Object.values(ctx.state.players).find((p) => (p.resources['life'] ?? 0) <= 0);
106
+ if (!fallen) return { gameOver: false };
107
+ return {
108
+ gameOver: true,
109
+ winners: Object.values(ctx.state.players).filter((p) => p.playerId !== fallen.playerId).map((p) => p.playerId),
110
+ reason: 'life',
111
+ };
112
+ },
113
+ legalMoves: (state, playerId) =>
114
+ state.result || state.priority.holder !== playerId
115
+ ? []
116
+ : [{ playerId, type: 'action', data: { type: 'action', actionTypeId: FIRE_BOLT, targets: {}, payload: {} } }],
117
+ };
118
+
119
+ export default plugin;
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "__SLUG__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "check": "game-kit check",
8
+ "test": "game-kit test",
9
+ "simulate": "game-kit simulate",
10
+ "preview": "game-kit preview"
11
+ },
12
+ "devDependencies": {
13
+ "@table-games-engine/core": "^0.1.1",
14
+ "@table-games-engine/game-kit": "^0.1.1",
15
+ "typescript": "^5.4.0",
16
+ "vite": "^5.4.0",
17
+ "vitest": "^2.0.0"
18
+ }
19
+ }
@@ -0,0 +1,5 @@
1
+ # __NAME__
2
+
3
+ Two players start with the life set by the match preset (5 on *Standard*, 3 on *Quick*).
4
+ On your turn you cast **Fire Bolt**: your opponent loses 2 life and the turn passes.
5
+ The first player at 0 life or below loses.
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { driveAction, rulesFromPlugin, setupMatch, type LegalMove, type PlayerId } from '@table-games-engine/core';
3
+ import plugin, { FIRE_BOLT } from '../logic/index';
4
+
5
+ const P1 = 'p1' as PlayerId;
6
+ const P2 = 'p2' as PlayerId;
7
+
8
+ /** Typed access to a move's action fields (actionTypeId, payload): no casts needed. */
9
+ const actionOf = (m: LegalMove) => (m.data.type === 'action' ? m.data : null);
10
+
11
+ describe('fire duel', () => {
12
+ it('the first player wins the straight race on the standard preset', () => {
13
+ let state = setupMatch(plugin, { playerIds: [P1, P2], config: { life: 5 }, seed: 'test' });
14
+ const rules = rulesFromPlugin(plugin);
15
+ for (let step = 0; !state.result && step < 10; step++) {
16
+ const move = plugin.legalMoves!(state, state.priority.holder!).find((m) => actionOf(m)?.actionTypeId === FIRE_BOLT);
17
+ const r = driveAction(rules, state, { ...move!, clientId: `t${step}`, stateVersion: state.version as unknown as number });
18
+ if (!r.ok) throw new Error(JSON.stringify(r.error));
19
+ state = r.state;
20
+ }
21
+ expect(state.result?.winners).toEqual([P1]);
22
+ });
23
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler",
4
+ "lib": ["ES2022", "DOM"], "strict": true, "noEmit": true, "skipLibCheck": true
5
+ },
6
+ "include": ["logic/**/*.ts", "client/**/*.ts", "tests/**/*.ts"]
7
+ }