bga-dev-skill 0.1.0
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.
- package/CC_PLAN.md +781 -0
- package/README.md +59 -0
- package/SKILL.md +117 -0
- package/composer.json +13 -0
- package/harness/example/SampleGame.php +160 -0
- package/harness/example/SampleGameTest.php +104 -0
- package/harness/example/sampleUtils.test.js +53 -0
- package/harness/js/bgaStubs.js +32 -0
- package/harness/js/testHelpers.js +9 -0
- package/harness/php/BgaDatabaseFake.php +234 -0
- package/harness/php/BgaExceptionTypes.php +17 -0
- package/harness/php/BgaGameTestCase.php +160 -0
- package/harness/php/BgaNotificationSpy.php +116 -0
- package/harness/php/BgaStubs.php +244 -0
- package/jest.config.js +5 -0
- package/package.json +11 -0
- package/skills/database-patterns.md +143 -0
- package/skills/js-dojo-patterns.md +89 -0
- package/skills/notifications.md +321 -0
- package/skills/scaffold-templates.md +82 -0
- package/skills/state-machine.md +126 -0
- package/src/setgame.utils.js +47 -0
package/CC_PLAN.md
ADDED
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
# BGA Dev Skill & Testing Harness — Claude Code Build Plan
|
|
2
|
+
|
|
3
|
+
## What This Repo Is
|
|
4
|
+
|
|
5
|
+
A publishable GitHub repo that makes Claude Code (CC) immediately useful for
|
|
6
|
+
BoardGameArena game development. Two deliverables in one repo:
|
|
7
|
+
|
|
8
|
+
1. **SKILL.md** — A CC skill file encoding BGA conventions, API patterns, and
|
|
9
|
+
code generation rules. Users drop this into their own workspace and CC
|
|
10
|
+
becomes BGA-aware without any infrastructure.
|
|
11
|
+
|
|
12
|
+
2. **Testing Harness** — A PHP stub library (PHPUnit) and JS utility layer
|
|
13
|
+
(Jest) that let CC generate and run meaningful tests against real game logic,
|
|
14
|
+
locally, without a live Studio slot.
|
|
15
|
+
|
|
16
|
+
An optional third layer — Playwright integration for E2E tests against a live
|
|
17
|
+
Studio slot — is scoped as a future v2 and should NOT be built in v1.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Repo Structure to Create
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
bga-dev-skill/
|
|
25
|
+
│
|
|
26
|
+
├── SKILL.md # Primary skill file — CC reads this
|
|
27
|
+
│
|
|
28
|
+
├── skills/ # Sub-skills referenced from SKILL.md
|
|
29
|
+
│ ├── state-machine.md # State machine patterns + worked examples
|
|
30
|
+
│ ├── database-patterns.md # DbQuery, getCollectionFromDB, SQL schema
|
|
31
|
+
│ ├── js-dojo-patterns.md # Dojo module patterns, ajaxcall, notifications
|
|
32
|
+
│ ├── notifications.md # notifyAllPlayers / notifyPlayer contracts
|
|
33
|
+
│ └── scaffold-templates.md # Boilerplate for each required BGA file
|
|
34
|
+
│
|
|
35
|
+
├── harness/
|
|
36
|
+
│ ├── php/
|
|
37
|
+
│ │ ├── BgaStubs.php # Mock BGA framework methods
|
|
38
|
+
│ │ ├── BgaGameTestCase.php # Base test class with fluent helpers
|
|
39
|
+
│ │ ├── BgaNotificationSpy.php # Captures and asserts notifications
|
|
40
|
+
│ │ ├── BgaDatabaseFake.php # In-memory DB stand-in for DbQuery etc.
|
|
41
|
+
│ │ └── BgaExceptionTypes.php # BGA exception class stubs
|
|
42
|
+
│ ├── js/
|
|
43
|
+
│ │ ├── bgaStubs.js # Mock dojo, gameui, BGA globals
|
|
44
|
+
│ │ └── testHelpers.js # Assertion helpers for game state
|
|
45
|
+
│ └── example/
|
|
46
|
+
│ ├── SampleGame.php # Minimal example game using harness
|
|
47
|
+
│ ├── SampleGameTest.php # Annotated tests CC can learn from
|
|
48
|
+
│ └── sampleUtils.test.js # Example Jest tests for JS logic
|
|
49
|
+
│
|
|
50
|
+
├── composer.json # PHPUnit dependency
|
|
51
|
+
├── package.json # Jest dependency
|
|
52
|
+
├── phpunit.xml # PHPUnit config pointing at harness + example
|
|
53
|
+
├── jest.config.js # Jest config
|
|
54
|
+
└── README.md # Install instructions for end users
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Build Order
|
|
60
|
+
|
|
61
|
+
Work through these phases in sequence. Each phase should be fully working and
|
|
62
|
+
committed before starting the next. CC should not jump ahead.
|
|
63
|
+
|
|
64
|
+
### Phase 1 — Repo Scaffolding
|
|
65
|
+
### Phase 2 — PHP Stub Library
|
|
66
|
+
### Phase 3 — Base Test Case (fluent helpers)
|
|
67
|
+
### Phase 4 — SKILL.md (main skill file)
|
|
68
|
+
### Phase 5 — Sub-skill files
|
|
69
|
+
### Phase 6 — Example game + annotated tests
|
|
70
|
+
### Phase 7 — JS stub layer + Jest config
|
|
71
|
+
### Phase 8 — README
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Phase 1: Repo Scaffolding
|
|
76
|
+
|
|
77
|
+
Create the directory structure above. Create all files as empty stubs with a
|
|
78
|
+
single comment indicating what they will contain. This gives CC the full file
|
|
79
|
+
tree to reason about from the start.
|
|
80
|
+
|
|
81
|
+
Create `composer.json`:
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"name": "your-handle/bga-dev-skill",
|
|
85
|
+
"description": "CC skill and testing harness for BoardGameArena game development",
|
|
86
|
+
"require-dev": {
|
|
87
|
+
"phpunit/phpunit": "^10.0"
|
|
88
|
+
},
|
|
89
|
+
"autoload": {
|
|
90
|
+
"psr-4": {
|
|
91
|
+
"BgaHarness\\": "harness/php/",
|
|
92
|
+
"BgaExample\\": "harness/example/"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Create `package.json`:
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"name": "bga-dev-skill",
|
|
102
|
+
"scripts": {
|
|
103
|
+
"test": "jest"
|
|
104
|
+
},
|
|
105
|
+
"devDependencies": {
|
|
106
|
+
"jest": "^29.0.0"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Create `phpunit.xml` pointing at `harness/` with colors enabled.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Phase 2: PHP Stub Library
|
|
116
|
+
|
|
117
|
+
This is the most critical phase. The stubs must accurately reflect BGA's actual
|
|
118
|
+
API so CC doesn't hallucinate method signatures when generating tests.
|
|
119
|
+
|
|
120
|
+
### `BgaDatabaseFake.php`
|
|
121
|
+
|
|
122
|
+
An in-memory SQLite-backed (or array-backed) fake for BGA's DB layer. Must
|
|
123
|
+
implement these methods with identical signatures to BGA's real framework:
|
|
124
|
+
|
|
125
|
+
```php
|
|
126
|
+
namespace BgaHarness;
|
|
127
|
+
|
|
128
|
+
class BgaDatabaseFake {
|
|
129
|
+
private array $tables = [];
|
|
130
|
+
|
|
131
|
+
// Executes a query string — for INSERT, UPDATE, DELETE
|
|
132
|
+
public function DbQuery(string $sql): void {}
|
|
133
|
+
|
|
134
|
+
// Returns array of rows — equivalent to BGA's getCollectionFromDB
|
|
135
|
+
public function getCollectionFromDB(string $sql, bool $bUniqueValue = false): array {}
|
|
136
|
+
|
|
137
|
+
// Returns single row
|
|
138
|
+
public function getObjectFromDB(string $sql): ?array {}
|
|
139
|
+
|
|
140
|
+
// Returns single value
|
|
141
|
+
public function getUniqueValueFromDB(string $sql): mixed {}
|
|
142
|
+
|
|
143
|
+
// Returns count
|
|
144
|
+
public function getIntFromDB(string $sql): int {}
|
|
145
|
+
|
|
146
|
+
// Seed the fake with table data for test setup
|
|
147
|
+
public function seedTable(string $table, array $rows): void {}
|
|
148
|
+
|
|
149
|
+
// Assert a row exists matching conditions
|
|
150
|
+
public function assertRowExists(string $table, array $conditions): void {}
|
|
151
|
+
|
|
152
|
+
// Assert row count
|
|
153
|
+
public function assertRowCount(string $table, int $expected, array $conditions = []): void {}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Implementation note: Use PHP's PDO with SQLite `:memory:` as the backing store.
|
|
158
|
+
This lets real SQL run against the fake, catching SQL errors in tests.
|
|
159
|
+
|
|
160
|
+
### `BgaNotificationSpy.php`
|
|
161
|
+
|
|
162
|
+
Captures all notifications emitted during a test for later assertion:
|
|
163
|
+
|
|
164
|
+
```php
|
|
165
|
+
namespace BgaHarness;
|
|
166
|
+
|
|
167
|
+
class BgaNotificationSpy {
|
|
168
|
+
private array $notifications = [];
|
|
169
|
+
|
|
170
|
+
public function notifyAllPlayers(string $type, string $message, array $data): void {
|
|
171
|
+
$this->notifications[] = [
|
|
172
|
+
'target' => 'all',
|
|
173
|
+
'type' => $type,
|
|
174
|
+
'message' => $message,
|
|
175
|
+
'data' => $data,
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public function notifyPlayer(int $playerId, string $type, string $message, array $data): void {
|
|
180
|
+
$this->notifications[] = [
|
|
181
|
+
'target' => $playerId,
|
|
182
|
+
'type' => $type,
|
|
183
|
+
'message' => $message,
|
|
184
|
+
'data' => $data,
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Assertions
|
|
189
|
+
public function assertNotified(string $type, array $dataSubset = []): void {}
|
|
190
|
+
public function assertNotifiedPlayer(int $playerId, string $type, array $dataSubset = []): void {}
|
|
191
|
+
public function assertNotNotified(string $type): void {}
|
|
192
|
+
public function assertNotificationCount(int $expected): void {}
|
|
193
|
+
public function getNotifications(): array { return $this->notifications; }
|
|
194
|
+
public function reset(): void { $this->notifications = []; }
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `BgaStubs.php`
|
|
199
|
+
|
|
200
|
+
The main framework stub. Game classes extend `Table` in BGA — this stub
|
|
201
|
+
replaces that base class for testing:
|
|
202
|
+
|
|
203
|
+
```php
|
|
204
|
+
namespace BgaHarness;
|
|
205
|
+
|
|
206
|
+
abstract class BgaStubs {
|
|
207
|
+
|
|
208
|
+
protected BgaDatabaseFake $db;
|
|
209
|
+
protected BgaNotificationSpy $notifications;
|
|
210
|
+
|
|
211
|
+
// Player state
|
|
212
|
+
private int $activePlayerId;
|
|
213
|
+
private array $players = [];
|
|
214
|
+
private string $currentState = 'gameSetup';
|
|
215
|
+
|
|
216
|
+
public function __construct() {
|
|
217
|
+
$this->db = new BgaDatabaseFake();
|
|
218
|
+
$this->notifications = new BgaNotificationSpy();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Player management ──────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
protected function getActivePlayerId(): int {}
|
|
224
|
+
protected function getActivePlayerName(): string {}
|
|
225
|
+
protected function getPlayerNameById(int $playerId): string {}
|
|
226
|
+
protected function getCurrentPlayerId(): int {}
|
|
227
|
+
|
|
228
|
+
// ── State machine ──────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
protected function gamestate_nextState(string $transition): void {}
|
|
231
|
+
protected function gamestate_changeActivePlayer(int $playerId): void {}
|
|
232
|
+
protected function gamestate_setAllPlayersMultiactive(): void {}
|
|
233
|
+
protected function gamestate_setPlayerNonMultiactive(int $playerId, string $nextState): void {}
|
|
234
|
+
protected function checkAction(string $actionName): bool {} // validates vs state machine
|
|
235
|
+
|
|
236
|
+
// ── Database pass-throughs ─────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
protected function DbQuery(string $sql): void {
|
|
239
|
+
$this->db->DbQuery($sql);
|
|
240
|
+
}
|
|
241
|
+
protected function getCollectionFromDB(string $sql, bool $bUniqueValue = false): array {
|
|
242
|
+
return $this->db->getCollectionFromDB($sql, $bUniqueValue);
|
|
243
|
+
}
|
|
244
|
+
protected function getObjectFromDB(string $sql): ?array {
|
|
245
|
+
return $this->db->getObjectFromDB($sql);
|
|
246
|
+
}
|
|
247
|
+
protected function getUniqueValueFromDB(string $sql): mixed {
|
|
248
|
+
return $this->db->getUniqueValueFromDB($sql);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Notifications pass-throughs ────────────────────────────────
|
|
252
|
+
|
|
253
|
+
protected function notifyAllPlayers(string $type, string $msg, array $data): void {
|
|
254
|
+
$this->notifications->notifyAllPlayers($type, $msg, $data);
|
|
255
|
+
}
|
|
256
|
+
protected function notifyPlayer(int $id, string $type, string $msg, array $data): void {
|
|
257
|
+
$this->notifications->notifyPlayer($id, $type, $msg, $data);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── BGA utility methods ────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
protected function getGameStateValue(string $name): int {}
|
|
263
|
+
protected function setGameStateValue(string $name, int $value): void {}
|
|
264
|
+
protected function incGameStateValue(string $name, int $increment): int {}
|
|
265
|
+
|
|
266
|
+
// ── Cards (BGA Deck component stub) ───────────────────────────
|
|
267
|
+
|
|
268
|
+
protected function createDeck(string $deckId): BgaDeckStub {}
|
|
269
|
+
|
|
270
|
+
// ── Error handling ─────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
// BGA uses these — tests can assert they are (or aren't) thrown
|
|
273
|
+
protected function throwUserError(string $errorCode): never {
|
|
274
|
+
throw new BgaUserException($errorCode);
|
|
275
|
+
}
|
|
276
|
+
protected function throwVisibleSystemError(string $message): never {
|
|
277
|
+
throw new BgaVisibleSystemException($message);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── Test setup helpers (not in real BGA — test-only) ──────────
|
|
281
|
+
|
|
282
|
+
public function _setActivePlayer(int $playerId): void {}
|
|
283
|
+
public function _setPlayers(array $players): void {}
|
|
284
|
+
public function _setState(string $stateName): void {}
|
|
285
|
+
public function _getDb(): BgaDatabaseFake { return $this->db; }
|
|
286
|
+
public function _getNotifications(): BgaNotificationSpy { return $this->notifications; }
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### `BgaExceptionTypes.php`
|
|
291
|
+
|
|
292
|
+
```php
|
|
293
|
+
namespace BgaHarness;
|
|
294
|
+
|
|
295
|
+
// BGA throws these — stubs so test code can catch them
|
|
296
|
+
class BgaUserException extends \Exception {}
|
|
297
|
+
class BgaVisibleSystemException extends \Exception {}
|
|
298
|
+
class BgaSystemException extends \Exception {}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Phase 3: Base Test Case
|
|
304
|
+
|
|
305
|
+
A fluent test case base class that CC should use as the template for every
|
|
306
|
+
generated test. The fluent style is important — it makes CC's output from
|
|
307
|
+
natural language queries readable and self-documenting.
|
|
308
|
+
|
|
309
|
+
```php
|
|
310
|
+
namespace BgaHarness;
|
|
311
|
+
|
|
312
|
+
use PHPUnit\Framework\TestCase;
|
|
313
|
+
|
|
314
|
+
abstract class BgaGameTestCase extends TestCase {
|
|
315
|
+
|
|
316
|
+
protected BgaStubs $game; // subclasses set this to their game instance
|
|
317
|
+
|
|
318
|
+
protected function setUp(): void {
|
|
319
|
+
parent::setUp();
|
|
320
|
+
$this->game = $this->createGame();
|
|
321
|
+
$this->game->_setPlayers($this->defaultPlayers());
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Subclass implements this to return their game instance
|
|
325
|
+
abstract protected function createGame(): BgaStubs;
|
|
326
|
+
|
|
327
|
+
// ── Given (setup) ──────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
protected function givenActivePlayer(int $playerId): static {
|
|
330
|
+
$this->game->_setActivePlayer($playerId);
|
|
331
|
+
return $this;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
protected function givenState(string $stateName): static {
|
|
335
|
+
$this->game->_setState($stateName);
|
|
336
|
+
return $this;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
protected function givenDatabaseRows(string $table, array $rows): static {
|
|
340
|
+
$this->game->_getDb()->seedTable($table, $rows);
|
|
341
|
+
return $this;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
protected function givenGameStateValue(string $name, int $value): static {
|
|
345
|
+
// seeds a game state variable
|
|
346
|
+
return $this;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ── When (action) ──────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
// Call a player action method — wraps in try/catch for error assertions
|
|
352
|
+
protected function whenAction(string $method, array $args = []): ActionResult {
|
|
353
|
+
try {
|
|
354
|
+
$result = $this->game->$method(...array_values($args));
|
|
355
|
+
return ActionResult::success($result);
|
|
356
|
+
} catch (BgaUserException $e) {
|
|
357
|
+
return ActionResult::userError($e->getMessage());
|
|
358
|
+
} catch (BgaVisibleSystemException $e) {
|
|
359
|
+
return ActionResult::systemError($e->getMessage());
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── Then (assertion) ───────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
protected function thenStateShouldBe(string $expected): void {
|
|
366
|
+
// read current state from game and assert
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
protected function thenNotificationSent(string $type, array $dataSubset = []): void {
|
|
370
|
+
$this->game->_getNotifications()->assertNotified($type, $dataSubset);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
protected function thenNotificationNotSent(string $type): void {
|
|
374
|
+
$this->game->_getNotifications()->assertNotNotified($type);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
protected function thenPlayerNotifiedWith(int $playerId, string $type, array $dataSubset = []): void {
|
|
378
|
+
$this->game->_getNotifications()->assertNotifiedPlayer($playerId, $type, $dataSubset);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
protected function thenDatabaseHas(string $table, array $conditions): void {
|
|
382
|
+
$this->game->_getDb()->assertRowExists($table, $conditions);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
protected function thenDatabaseCount(string $table, int $count, array $conditions = []): void {
|
|
386
|
+
$this->game->_getDb()->assertRowCount($table, $count, $conditions);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ── Defaults ───────────────────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
protected function defaultPlayers(): array {
|
|
392
|
+
return [
|
|
393
|
+
1 => ['player_id' => 1, 'player_name' => 'Alice', 'player_score' => 0],
|
|
394
|
+
2 => ['player_id' => 2, 'player_name' => 'Bob', 'player_score' => 0],
|
|
395
|
+
];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Wraps action results so tests can assert on success or specific error type
|
|
400
|
+
class ActionResult {
|
|
401
|
+
public bool $succeeded;
|
|
402
|
+
public ?string $errorCode;
|
|
403
|
+
public mixed $returnValue;
|
|
404
|
+
|
|
405
|
+
public static function success(mixed $value): self {}
|
|
406
|
+
public static function userError(string $code): self {}
|
|
407
|
+
public static function systemError(string $message): self {}
|
|
408
|
+
|
|
409
|
+
public function assertSucceeded(): void {}
|
|
410
|
+
public function assertFailedWith(string $errorCode): void {}
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## Phase 4: SKILL.md
|
|
417
|
+
|
|
418
|
+
This is what CC reads when working in a BGA game project. It should be opinionated
|
|
419
|
+
and direct — written as instructions to CC, not as documentation for humans.
|
|
420
|
+
|
|
421
|
+
Structure:
|
|
422
|
+
|
|
423
|
+
```markdown
|
|
424
|
+
---
|
|
425
|
+
name: bga-dev-skill
|
|
426
|
+
description: BGA game development patterns, API reference, and testing conventions
|
|
427
|
+
for BoardGameArena games. Provides correct method signatures, state machine
|
|
428
|
+
patterns, and test generation for PHP game logic and JS frontend code.
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
# BGA Development
|
|
432
|
+
|
|
433
|
+
## Your Role
|
|
434
|
+
When working in a BGA project, you are a BGA-experienced developer who knows
|
|
435
|
+
the framework's constraints and idioms. Never suggest raw PDO, never use
|
|
436
|
+
superglobals, never call BGA methods that don't exist. When generating tests,
|
|
437
|
+
always use BgaGameTestCase and the fluent pattern.
|
|
438
|
+
|
|
439
|
+
## File Structure (every BGA game has exactly these files)
|
|
440
|
+
[list all required files with their purpose]
|
|
441
|
+
|
|
442
|
+
## PHP Server — Critical Rules
|
|
443
|
+
[BGA DB layer — only these methods exist]
|
|
444
|
+
[State machine — how transitions work]
|
|
445
|
+
[Error throwing — only these two methods]
|
|
446
|
+
[What self:: calls are valid]
|
|
447
|
+
|
|
448
|
+
## PHP Server — Common Patterns
|
|
449
|
+
[Card dealing from Deck component]
|
|
450
|
+
[Multi-active player handling]
|
|
451
|
+
[Scoring patterns]
|
|
452
|
+
[Undo-safe action patterns]
|
|
453
|
+
|
|
454
|
+
## JS Frontend — Critical Rules
|
|
455
|
+
[Dojo module declaration pattern]
|
|
456
|
+
[ajaxcall signature and error handling]
|
|
457
|
+
[Notification handler registration]
|
|
458
|
+
[Never use document.querySelector — use dojo.query]
|
|
459
|
+
|
|
460
|
+
## State Machine
|
|
461
|
+
[states.inc.php structure]
|
|
462
|
+
[Transition naming conventions]
|
|
463
|
+
[Multi-active state patterns]
|
|
464
|
+
[When to use activeplayer vs multipleactiveplayer]
|
|
465
|
+
|
|
466
|
+
## Testing — Generating Tests from Natural Language
|
|
467
|
+
When asked "what happens when [scenario]", generate a PHPUnit test using
|
|
468
|
+
BgaGameTestCase with this exact pattern:
|
|
469
|
+
[worked example]
|
|
470
|
+
|
|
471
|
+
When asked to test a JS utility function, generate a Jest test with this pattern:
|
|
472
|
+
[worked example]
|
|
473
|
+
|
|
474
|
+
## Testing — What Can and Cannot Be Tested
|
|
475
|
+
CAN test: game logic, state transitions, DB writes, notification contents
|
|
476
|
+
CANNOT test without a live Studio slot: rendering, animations, ajaxcall network
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## Phase 5: Sub-skill Files
|
|
482
|
+
|
|
483
|
+
### `skills/state-machine.md`
|
|
484
|
+
|
|
485
|
+
Full worked example of a states.inc.php for a 3-state game with:
|
|
486
|
+
- `gameSetup` → `playerTurn` → `endGame`
|
|
487
|
+
- Multi-active voting state example
|
|
488
|
+
- All required keys documented with correct types
|
|
489
|
+
- Common mistake: forgetting `possibleactions` in non-game-end states
|
|
490
|
+
|
|
491
|
+
### `skills/database-patterns.md`
|
|
492
|
+
|
|
493
|
+
Correct SQL and PHP for the 6 most common BGA database operations:
|
|
494
|
+
- Dealing cards from deck to hand
|
|
495
|
+
- Moving cards between locations
|
|
496
|
+
- Incrementing player score
|
|
497
|
+
- Getting all cards in a location
|
|
498
|
+
- Getting a specific player's hand count
|
|
499
|
+
- Checking if a player has a specific card
|
|
500
|
+
|
|
501
|
+
Each pattern shown as both raw SQL (for `DbQuery`) and as the recommended
|
|
502
|
+
`getCollectionFromDB` / `getObjectFromDB` call.
|
|
503
|
+
|
|
504
|
+
### `skills/js-dojo-patterns.md`
|
|
505
|
+
|
|
506
|
+
- Correct `define([...], function(...) { ... })` module pattern
|
|
507
|
+
- `ajaxcall` with action, args, handler, and error handler
|
|
508
|
+
- `addTooltip` and `addTooltipToClass`
|
|
509
|
+
- `dojo.place`, `dojo.query`, `dojo.connect` vs `dojo.on`
|
|
510
|
+
- How to register a notification handler in `setupNotifications()`
|
|
511
|
+
- Animation with `dojo.animateProperty`
|
|
512
|
+
|
|
513
|
+
### `skills/notifications.md`
|
|
514
|
+
|
|
515
|
+
The notification contract between PHP and JS — this is where most BGA bugs live.
|
|
516
|
+
|
|
517
|
+
For each notification type:
|
|
518
|
+
- PHP: which method to call, data shape
|
|
519
|
+
- JS: how to register the handler, what `notif.args` contains
|
|
520
|
+
- Common mistake: PHP sends `card_id`, JS expects `cardId` (casing mismatch)
|
|
521
|
+
|
|
522
|
+
### `skills/scaffold-templates.md`
|
|
523
|
+
|
|
524
|
+
Minimal correct boilerplate for each required file. CC should use these as
|
|
525
|
+
starting points, not invent its own structure. Include:
|
|
526
|
+
- `gamename.game.php` — correct class extension, required method stubs
|
|
527
|
+
- `gamename.action.php` — correct action method pattern with sanitization
|
|
528
|
+
- `gamename.view.php` — correct view method
|
|
529
|
+
- `gamename.js` — correct Dojo define wrapper with required methods
|
|
530
|
+
- `states.inc.php` — minimal 2-state example
|
|
531
|
+
- `dbmodel.sql` — correct format with player table already included
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## Phase 6: Example Game + Annotated Tests
|
|
536
|
+
|
|
537
|
+
A minimal but complete game called `setgame` (a simplified Set card game).
|
|
538
|
+
This is the reference implementation CC learns test patterns from.
|
|
539
|
+
|
|
540
|
+
### `SampleGame.php`
|
|
541
|
+
|
|
542
|
+
Implements exactly:
|
|
543
|
+
- `setupNewGame()` — deals cards to players
|
|
544
|
+
- `action_playSet(array $cardIds)` — validates and plays a set
|
|
545
|
+
- `action_pass()` — player passes their turn
|
|
546
|
+
- `_checkValidSet(array $cards): bool` — pure logic, easily testable
|
|
547
|
+
- `_scoreSet(int $playerId, array $cards): void` — updates DB and notifies
|
|
548
|
+
|
|
549
|
+
### `SampleGameTest.php`
|
|
550
|
+
|
|
551
|
+
Must include annotated tests for all of these scenarios, with comments
|
|
552
|
+
explaining the pattern so CC can adapt them:
|
|
553
|
+
|
|
554
|
+
```php
|
|
555
|
+
// ── Happy path ─────────────────────────────────────────────────────
|
|
556
|
+
|
|
557
|
+
public function test_valid_set_scores_and_advances_state(): void {
|
|
558
|
+
// CC PATTERN: happy path — given setup → when action → then state + notification
|
|
559
|
+
$this->givenActivePlayer(1)
|
|
560
|
+
->givenState('playerTurn')
|
|
561
|
+
->givenDatabaseRows('card', [
|
|
562
|
+
['card_id' => 1, 'card_type' => 'red', 'card_location' => 'hand', 'card_location_arg' => 1],
|
|
563
|
+
['card_id' => 2, 'card_type' => 'green', 'card_location' => 'hand', 'card_location_arg' => 1],
|
|
564
|
+
['card_id' => 3, 'card_type' => 'blue', 'card_location' => 'hand', 'card_location_arg' => 1],
|
|
565
|
+
]);
|
|
566
|
+
|
|
567
|
+
$result = $this->whenAction('action_playSet', ['card_ids' => [1, 2, 3]]);
|
|
568
|
+
|
|
569
|
+
$result->assertSucceeded();
|
|
570
|
+
$this->thenNotificationSent('setPlayed', ['player_id' => 1, 'score' => 1]);
|
|
571
|
+
$this->thenDatabaseHas('card', ['card_location' => 'discard', 'card_id' => 1]);
|
|
572
|
+
$this->thenStateShouldBe('playerTurn'); // next player's turn
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ── Invalid input ──────────────────────────────────────────────────
|
|
576
|
+
|
|
577
|
+
public function test_invalid_set_rejected_with_error(): void {
|
|
578
|
+
// CC PATTERN: invalid action — assert specific BGA error code thrown
|
|
579
|
+
$this->givenActivePlayer(1)
|
|
580
|
+
->givenState('playerTurn')
|
|
581
|
+
->givenDatabaseRows('card', [/* two matching, one not */]);
|
|
582
|
+
|
|
583
|
+
$result = $this->whenAction('action_playSet', ['card_ids' => [1, 2, 3]]);
|
|
584
|
+
|
|
585
|
+
$result->assertFailedWith('invalidSet');
|
|
586
|
+
$this->thenNotificationNotSent('setPlayed');
|
|
587
|
+
$this->thenStateShouldBe('playerTurn'); // state unchanged
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ── Wrong player ───────────────────────────────────────────────────
|
|
591
|
+
|
|
592
|
+
public function test_inactive_player_cannot_play(): void {
|
|
593
|
+
// CC PATTERN: wrong actor — checkAction should block this
|
|
594
|
+
$this->givenActivePlayer(1)
|
|
595
|
+
->givenState('playerTurn');
|
|
596
|
+
|
|
597
|
+
// Player 2 tries to act when it's player 1's turn
|
|
598
|
+
$result = $this->whenAction('action_playSet', ['card_ids' => [4, 5, 6]]);
|
|
599
|
+
|
|
600
|
+
$result->assertFailedWith('notYourTurn');
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ── State transition ───────────────────────────────────────────────
|
|
604
|
+
|
|
605
|
+
public function test_last_set_triggers_endgame(): void {
|
|
606
|
+
// CC PATTERN: state transition — verify the right transition fires
|
|
607
|
+
$this->givenGameStateValue('cards_remaining', 3)
|
|
608
|
+
->givenActivePlayer(1);
|
|
609
|
+
|
|
610
|
+
$this->whenAction('action_playSet', ['card_ids' => [1, 2, 3]]);
|
|
611
|
+
|
|
612
|
+
$this->thenStateShouldBe('endGame');
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ── Pure logic ─────────────────────────────────────────────────────
|
|
616
|
+
|
|
617
|
+
public function test_set_validation_logic(): void {
|
|
618
|
+
// CC PATTERN: pure function — no given/when/then needed, just call directly
|
|
619
|
+
$validSet = [
|
|
620
|
+
['type' => 'red', 'number' => 1, 'shading' => 'solid'],
|
|
621
|
+
['type' => 'green', 'number' => 2, 'shading' => 'striped'],
|
|
622
|
+
['type' => 'blue', 'number' => 3, 'shading' => 'open'],
|
|
623
|
+
];
|
|
624
|
+
$this->assertTrue($this->game->_checkValidSet($validSet));
|
|
625
|
+
|
|
626
|
+
$invalidSet = $validSet;
|
|
627
|
+
$invalidSet[2]['type'] = 'red'; // two reds, not all different or all same
|
|
628
|
+
$this->assertFalse($this->game->_checkValidSet($invalidSet));
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
---
|
|
633
|
+
|
|
634
|
+
## Phase 7: JS Stub Layer
|
|
635
|
+
|
|
636
|
+
### `harness/js/bgaStubs.js`
|
|
637
|
+
|
|
638
|
+
Minimal Jest-compatible mocks for BGA globals:
|
|
639
|
+
|
|
640
|
+
```javascript
|
|
641
|
+
// Mocks the dojo global that BGA's JS assumes exists
|
|
642
|
+
global.dojo = {
|
|
643
|
+
place: jest.fn(),
|
|
644
|
+
query: jest.fn(() => ({ forEach: jest.fn(), length: 0 })),
|
|
645
|
+
style: jest.fn(),
|
|
646
|
+
addClass: jest.fn(),
|
|
647
|
+
removeClass: jest.fn(),
|
|
648
|
+
connect: jest.fn(),
|
|
649
|
+
on: jest.fn(),
|
|
650
|
+
animateProperty: jest.fn(() => ({ play: jest.fn() })),
|
|
651
|
+
require: jest.fn(),
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
// Mocks the gameui global
|
|
655
|
+
global.gameui = {
|
|
656
|
+
player_id: 1,
|
|
657
|
+
gamedatas: {},
|
|
658
|
+
addTooltip: jest.fn(),
|
|
659
|
+
ajaxcall: jest.fn(),
|
|
660
|
+
showMessage: jest.fn(),
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
// BGA notification mock — call triggerNotification in tests
|
|
664
|
+
const notificationHandlers = {};
|
|
665
|
+
global.bgaNotification = {
|
|
666
|
+
register: (type, handler) => { notificationHandlers[type] = handler; },
|
|
667
|
+
trigger: (type, args) => {
|
|
668
|
+
if (notificationHandlers[type]) notificationHandlers[type]({ args });
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### `jest.config.js`
|
|
674
|
+
|
|
675
|
+
```javascript
|
|
676
|
+
module.exports = {
|
|
677
|
+
testEnvironment: 'node',
|
|
678
|
+
setupFiles: ['./harness/js/bgaStubs.js'],
|
|
679
|
+
testMatch: ['**/*.test.js'],
|
|
680
|
+
};
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### `harness/example/sampleUtils.test.js`
|
|
684
|
+
|
|
685
|
+
Example Jest tests for extractable game logic:
|
|
686
|
+
|
|
687
|
+
```javascript
|
|
688
|
+
import { isValidSet, calculateScore, getLegalMoves } from '../../src/setgame.utils.js';
|
|
689
|
+
|
|
690
|
+
describe('isValidSet', () => {
|
|
691
|
+
test('all same color is valid', () => { /* ... */ });
|
|
692
|
+
test('all different colors is valid', () => { /* ... */ });
|
|
693
|
+
test('two same one different is invalid', () => { /* ... */ });
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
describe('calculateScore', () => {
|
|
697
|
+
test('base score for standard set', () => { /* ... */ });
|
|
698
|
+
test('bonus multiplier for 3-in-a-row', () => { /* ... */ });
|
|
699
|
+
});
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## Phase 8: README
|
|
705
|
+
|
|
706
|
+
Structure:
|
|
707
|
+
|
|
708
|
+
### Installation (two paths)
|
|
709
|
+
|
|
710
|
+
**Skill only** (no testing infrastructure needed):
|
|
711
|
+
```
|
|
712
|
+
1. Clone this repo
|
|
713
|
+
2. Add to your Claude Code workspace
|
|
714
|
+
3. Reference SKILL.md in your workspace settings
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
**Full harness**:
|
|
718
|
+
```
|
|
719
|
+
1. Clone this repo into your BGA game's dev dependencies
|
|
720
|
+
2. composer require --dev your-handle/bga-dev-skill
|
|
721
|
+
3. npm install --save-dev bga-dev-skill
|
|
722
|
+
4. Extend BgaGameTestCase in your tests
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
### Using with Claude Code
|
|
726
|
+
|
|
727
|
+
Show 5 example prompts that now work correctly:
|
|
728
|
+
|
|
729
|
+
```
|
|
730
|
+
"What happens if the active player submits an empty card selection?"
|
|
731
|
+
→ CC generates a PHPUnit test using BgaGameTestCase
|
|
732
|
+
|
|
733
|
+
"Write tests for the _calculateScore function"
|
|
734
|
+
→ CC generates both a PHP unit test and a Jest test
|
|
735
|
+
|
|
736
|
+
"Generate the states.inc.php for a game with a bidding phase and a resolution phase"
|
|
737
|
+
→ CC uses the state machine skill to produce correct structure
|
|
738
|
+
|
|
739
|
+
"I'm getting a PHP error on line 47 of my action file — what's wrong?"
|
|
740
|
+
→ CC has context about valid BGA action method patterns
|
|
741
|
+
|
|
742
|
+
"How do I notify only the active player when they draw a card?"
|
|
743
|
+
→ CC uses the notifications skill to give the exact notifyPlayer call
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
### Running Tests
|
|
747
|
+
|
|
748
|
+
```bash
|
|
749
|
+
# PHP
|
|
750
|
+
./vendor/bin/phpunit
|
|
751
|
+
|
|
752
|
+
# JS
|
|
753
|
+
npm test
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
---
|
|
757
|
+
|
|
758
|
+
## What CC Should NOT Build (Scope Boundaries)
|
|
759
|
+
|
|
760
|
+
- No MCP server (CC handles code gen natively)
|
|
761
|
+
- No SFTP deployment tooling (out of scope for v1)
|
|
762
|
+
- No log tailing (requires live Studio infrastructure)
|
|
763
|
+
- No Playwright tests (v2 — note in README as planned)
|
|
764
|
+
- No BGA Studio API integration of any kind
|
|
765
|
+
- No game-specific logic (harness is game-agnostic)
|
|
766
|
+
|
|
767
|
+
---
|
|
768
|
+
|
|
769
|
+
## Definition of Done
|
|
770
|
+
|
|
771
|
+
Each phase is done when:
|
|
772
|
+
- [ ] Files exist and contain real implementation (not stubs)
|
|
773
|
+
- [ ] `composer install && ./vendor/bin/phpunit` passes with no errors
|
|
774
|
+
- [ ] `npm install && npm test` passes with no errors
|
|
775
|
+
- [ ] The example game tests all pass against the example game
|
|
776
|
+
- [ ] CC can answer "what happens when a player plays an invalid set" by
|
|
777
|
+
generating a test that actually runs and fails correctly before the
|
|
778
|
+
fix, then passes after
|
|
779
|
+
|
|
780
|
+
The repo is ready to publish when all phases are complete and the README
|
|
781
|
+
install instructions work on a clean machine.
|