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
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace BgaHarness;
|
|
6
|
+
|
|
7
|
+
require_once __DIR__ . '/BgaExceptionTypes.php';
|
|
8
|
+
|
|
9
|
+
abstract class BgaStubs
|
|
10
|
+
{
|
|
11
|
+
protected BgaDatabaseFake $db;
|
|
12
|
+
protected BgaNotificationSpy $notifications;
|
|
13
|
+
|
|
14
|
+
private int $activePlayerId = 0;
|
|
15
|
+
private int $currentPlayerId = 0;
|
|
16
|
+
private array $players = [];
|
|
17
|
+
private string $currentState = 'gameSetup';
|
|
18
|
+
private array $gameStateValues = [];
|
|
19
|
+
private array $allowedActions = [];
|
|
20
|
+
private array $transitionMap = [];
|
|
21
|
+
private array $multiactivePlayers = [];
|
|
22
|
+
|
|
23
|
+
public function __construct()
|
|
24
|
+
{
|
|
25
|
+
$this->db = new BgaDatabaseFake();
|
|
26
|
+
$this->notifications = new BgaNotificationSpy();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
protected function getActivePlayerId(): int
|
|
30
|
+
{
|
|
31
|
+
return $this->activePlayerId;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected function getActivePlayerName(): string
|
|
35
|
+
{
|
|
36
|
+
return $this->getPlayerNameById($this->activePlayerId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
protected function getPlayerNameById(int $playerId): string
|
|
40
|
+
{
|
|
41
|
+
return $this->players[$playerId]['player_name'] ?? 'Unknown';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
protected function getCurrentPlayerId(): int
|
|
45
|
+
{
|
|
46
|
+
return $this->currentPlayerId;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
protected function gamestate_nextState(string $transition): void
|
|
50
|
+
{
|
|
51
|
+
if (isset($this->transitionMap[$this->currentState][$transition])) {
|
|
52
|
+
$this->currentState = $this->transitionMap[$this->currentState][$transition];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protected function gamestate_changeActivePlayer(int $playerId): void
|
|
57
|
+
{
|
|
58
|
+
$this->activePlayerId = $playerId;
|
|
59
|
+
$this->currentPlayerId = $playerId;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
protected function gamestate_setAllPlayersMultiactive(): void
|
|
63
|
+
{
|
|
64
|
+
$this->multiactivePlayers = array_keys($this->players);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
protected function gamestate_setPlayerNonMultiactive(int $playerId, string $nextState): void
|
|
68
|
+
{
|
|
69
|
+
$this->multiactivePlayers = array_values(
|
|
70
|
+
array_filter(
|
|
71
|
+
$this->multiactivePlayers,
|
|
72
|
+
static fn (int $id): bool => $id !== $playerId
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if ($this->multiactivePlayers === []) {
|
|
77
|
+
$this->currentState = $nextState;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
protected function checkAction(string $actionName): bool
|
|
82
|
+
{
|
|
83
|
+
if ($this->currentPlayerId !== $this->activePlayerId) {
|
|
84
|
+
$this->throwUserError('notYourTurn');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
$allowed = $this->allowedActions[$this->currentState] ?? [];
|
|
88
|
+
if ($allowed !== [] && !in_array($actionName, $allowed, true)) {
|
|
89
|
+
$this->throwUserError('actionNotAllowed');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
protected function DbQuery(string $sql): void
|
|
96
|
+
{
|
|
97
|
+
$this->db->DbQuery($sql);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
protected function getCollectionFromDB(string $sql, bool $bUniqueValue = false): array
|
|
101
|
+
{
|
|
102
|
+
return $this->db->getCollectionFromDB($sql, $bUniqueValue);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
protected function getObjectFromDB(string $sql): ?array
|
|
106
|
+
{
|
|
107
|
+
return $this->db->getObjectFromDB($sql);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
protected function getUniqueValueFromDB(string $sql): mixed
|
|
111
|
+
{
|
|
112
|
+
return $this->db->getUniqueValueFromDB($sql);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
protected function getIntFromDB(string $sql): int
|
|
116
|
+
{
|
|
117
|
+
return $this->db->getIntFromDB($sql);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
protected function notifyAllPlayers(string $type, string $msg, array $data): void
|
|
121
|
+
{
|
|
122
|
+
$this->notifications->notifyAllPlayers($type, $msg, $data);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
protected function notifyPlayer(int $id, string $type, string $msg, array $data): void
|
|
126
|
+
{
|
|
127
|
+
$this->notifications->notifyPlayer($id, $type, $msg, $data);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
protected function getGameStateValue(string $name): int
|
|
131
|
+
{
|
|
132
|
+
return (int) ($this->gameStateValues[$name] ?? 0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
protected function setGameStateValue(string $name, int $value): void
|
|
136
|
+
{
|
|
137
|
+
$this->gameStateValues[$name] = $value;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
protected function incGameStateValue(string $name, int $increment): int
|
|
141
|
+
{
|
|
142
|
+
$next = $this->getGameStateValue($name) + $increment;
|
|
143
|
+
$this->setGameStateValue($name, $next);
|
|
144
|
+
|
|
145
|
+
return $next;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
protected function createDeck(string $deckId): BgaDeckStub
|
|
149
|
+
{
|
|
150
|
+
return new BgaDeckStub($deckId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
protected function throwUserError(string $errorCode): never
|
|
154
|
+
{
|
|
155
|
+
throw new BgaUserException($errorCode);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
protected function throwVisibleSystemError(string $message): never
|
|
159
|
+
{
|
|
160
|
+
throw new BgaVisibleSystemException($message);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public function _setActivePlayer(int $playerId): void
|
|
164
|
+
{
|
|
165
|
+
$this->activePlayerId = $playerId;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public function _setCurrentPlayer(int $playerId): void
|
|
169
|
+
{
|
|
170
|
+
$this->currentPlayerId = $playerId;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public function _setPlayers(array $players): void
|
|
174
|
+
{
|
|
175
|
+
$this->players = $players;
|
|
176
|
+
$this->multiactivePlayers = [];
|
|
177
|
+
if ($this->activePlayerId === 0 && $players !== []) {
|
|
178
|
+
$first = (int) array_key_first($players);
|
|
179
|
+
$this->activePlayerId = $first;
|
|
180
|
+
$this->currentPlayerId = $first;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public function _setState(string $stateName): void
|
|
185
|
+
{
|
|
186
|
+
$this->currentState = $stateName;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public function _setAllowedActions(array $byState): void
|
|
190
|
+
{
|
|
191
|
+
$this->allowedActions = $byState;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public function _setTransitions(array $map): void
|
|
195
|
+
{
|
|
196
|
+
$this->transitionMap = $map;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public function _setGameStateValue(string $name, int $value): void
|
|
200
|
+
{
|
|
201
|
+
$this->setGameStateValue($name, $value);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public function _getState(): string
|
|
205
|
+
{
|
|
206
|
+
return $this->currentState;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public function _getDb(): BgaDatabaseFake
|
|
210
|
+
{
|
|
211
|
+
return $this->db;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
public function _getNotifications(): BgaNotificationSpy
|
|
215
|
+
{
|
|
216
|
+
return $this->notifications;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
class BgaDeckStub
|
|
221
|
+
{
|
|
222
|
+
private string $deckId;
|
|
223
|
+
private array $cards = [];
|
|
224
|
+
|
|
225
|
+
public function __construct(string $deckId)
|
|
226
|
+
{
|
|
227
|
+
$this->deckId = $deckId;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
public function addCard(array $card): void
|
|
231
|
+
{
|
|
232
|
+
$this->cards[] = $card;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
public function count(): int
|
|
236
|
+
{
|
|
237
|
+
return count($this->cards);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
public function getDeckId(): string
|
|
241
|
+
{
|
|
242
|
+
return $this->deckId;
|
|
243
|
+
}
|
|
244
|
+
}
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Database Patterns
|
|
2
|
+
|
|
3
|
+
Use only the database vocabulary implemented in the harness. Do not use raw PDO in game classes.
|
|
4
|
+
|
|
5
|
+
## Method Vocabulary
|
|
6
|
+
|
|
7
|
+
Use these methods exactly:
|
|
8
|
+
|
|
9
|
+
- `DbQuery(string $sql): void`
|
|
10
|
+
- `getCollectionFromDB(string $sql, bool $bUniqueValue = false): array`
|
|
11
|
+
- `getObjectFromDB(string $sql): ?array`
|
|
12
|
+
- `getUniqueValueFromDB(string $sql): mixed`
|
|
13
|
+
- `getIntFromDB(string $sql): int`
|
|
14
|
+
|
|
15
|
+
## Pattern: Write with DbQuery
|
|
16
|
+
|
|
17
|
+
Use `DbQuery` for INSERT/UPDATE/DELETE and DDL.
|
|
18
|
+
|
|
19
|
+
```php
|
|
20
|
+
$this->DbQuery(
|
|
21
|
+
"CREATE TABLE IF NOT EXISTS card (" .
|
|
22
|
+
"card_id INTEGER PRIMARY KEY, " .
|
|
23
|
+
"card_type TEXT, " .
|
|
24
|
+
"card_number INTEGER, " .
|
|
25
|
+
"card_shading TEXT, " .
|
|
26
|
+
"card_location TEXT, " .
|
|
27
|
+
"card_location_arg INTEGER)"
|
|
28
|
+
);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```php
|
|
32
|
+
$this->DbQuery("UPDATE card SET card_location = 'discard' WHERE card_id = " . (int) $cardId);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Pattern: Read many rows
|
|
36
|
+
|
|
37
|
+
Use `getCollectionFromDB` for list queries.
|
|
38
|
+
|
|
39
|
+
```php
|
|
40
|
+
$cards = $this->getCollectionFromDB(
|
|
41
|
+
"SELECT card_id, card_type, card_number, card_shading FROM card WHERE card_location = 'deck' ORDER BY card_id LIMIT 3"
|
|
42
|
+
);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If you need key => value shape and query returns two columns, use `bUniqueValue = true`.
|
|
46
|
+
|
|
47
|
+
```php
|
|
48
|
+
$counts = $this->getCollectionFromDB(
|
|
49
|
+
"SELECT card_location_arg, COUNT(*) FROM card WHERE card_location = 'hand' GROUP BY card_location_arg",
|
|
50
|
+
true
|
|
51
|
+
);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Pattern: Read one row
|
|
55
|
+
|
|
56
|
+
Use `getObjectFromDB` when one row is expected.
|
|
57
|
+
|
|
58
|
+
```php
|
|
59
|
+
$row = $this->getObjectFromDB('SELECT * FROM card WHERE card_id = ' . (int) $cardId);
|
|
60
|
+
if ($row === null) {
|
|
61
|
+
$this->throwUserError('invalidSet');
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Pattern: Read one scalar
|
|
66
|
+
|
|
67
|
+
Use `getUniqueValueFromDB` for scalar values and cast explicitly when needed.
|
|
68
|
+
|
|
69
|
+
```php
|
|
70
|
+
$score = (int) $this->getUniqueValueFromDB('SELECT player_score FROM player WHERE player_id = ' . $playerId);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Use `getIntFromDB` for count/int-only reads.
|
|
74
|
+
|
|
75
|
+
```php
|
|
76
|
+
$remaining = $this->getIntFromDB("SELECT COUNT(*) FROM card WHERE card_location = 'deck'");
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Pattern: Dealing cards from deck to hand
|
|
80
|
+
|
|
81
|
+
```php
|
|
82
|
+
$cardsToDeal = $this->getCollectionFromDB(
|
|
83
|
+
"SELECT card_id FROM card WHERE card_location = 'deck' ORDER BY card_id LIMIT 3"
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
foreach ($cardsToDeal as $card) {
|
|
87
|
+
$this->DbQuery(sprintf(
|
|
88
|
+
"UPDATE card SET card_location = 'hand', card_location_arg = %d WHERE card_id = %d",
|
|
89
|
+
$playerId,
|
|
90
|
+
(int) $card['card_id']
|
|
91
|
+
));
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Deck Component Methods (Harness)
|
|
96
|
+
|
|
97
|
+
In this harness, deck usage is intentionally minimal:
|
|
98
|
+
|
|
99
|
+
- `createDeck(string $deckId): BgaDeckStub`
|
|
100
|
+
- `BgaDeckStub::addCard(array $card): void`
|
|
101
|
+
- `BgaDeckStub::count(): int`
|
|
102
|
+
- `BgaDeckStub::getDeckId(): string`
|
|
103
|
+
|
|
104
|
+
Use this stub only for in-memory deck behavior in tests or simplified examples.
|
|
105
|
+
|
|
106
|
+
## Numeric Cast Pattern
|
|
107
|
+
|
|
108
|
+
DB values can arrive as strings. Cast numeric fields before arithmetic/comparison.
|
|
109
|
+
|
|
110
|
+
```php
|
|
111
|
+
$players = $this->getCollectionFromDB("SELECT player_id id, player_score score FROM player");
|
|
112
|
+
foreach ($players as $id => $player) {
|
|
113
|
+
foreach ($player as $key => $value) {
|
|
114
|
+
if (preg_match('/^-?\\d+$/', (string) $value)) {
|
|
115
|
+
$players[$id][$key] = (int) $value;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## SQL Injection Warning
|
|
122
|
+
|
|
123
|
+
Never concatenate unsanitized user input into SQL strings.
|
|
124
|
+
|
|
125
|
+
Rules:
|
|
126
|
+
- Cast numeric IDs with `(int)` before interpolation.
|
|
127
|
+
- Validate string inputs against a known allow-list before interpolation.
|
|
128
|
+
- If a freeform string must be interpolated, sanitize first (`addslashes`) and document why.
|
|
129
|
+
|
|
130
|
+
Bad:
|
|
131
|
+
|
|
132
|
+
```php
|
|
133
|
+
$this->getCollectionFromDB("SELECT * FROM card WHERE card_type = '$type'");
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Good:
|
|
137
|
+
|
|
138
|
+
```php
|
|
139
|
+
if (!in_array($type, ['red', 'green', 'blue'], true)) {
|
|
140
|
+
$this->throwUserError('invalidType');
|
|
141
|
+
}
|
|
142
|
+
$this->getCollectionFromDB("SELECT * FROM card WHERE card_type = '$type'");
|
|
143
|
+
```
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# JS Patterns
|
|
2
|
+
|
|
3
|
+
Use this file as direct coding instructions for frontend behavior. The harness includes test stubs for legacy globals (`dojo`, `gameui`), while real modern projects can use `bga.*` APIs.
|
|
4
|
+
|
|
5
|
+
## What Is Implemented in This Repo
|
|
6
|
+
|
|
7
|
+
From harness files:
|
|
8
|
+
|
|
9
|
+
- `harness/js/bgaStubs.js` provides test doubles for:
|
|
10
|
+
- `dojo.*`
|
|
11
|
+
- `gameui.*`
|
|
12
|
+
- notification registration/triggering
|
|
13
|
+
- `harness/example/sampleUtils.test.js` demonstrates Jest tests for pure utility functions.
|
|
14
|
+
|
|
15
|
+
Use this to keep UI-independent logic testable.
|
|
16
|
+
|
|
17
|
+
## Modern Game Class Pattern
|
|
18
|
+
|
|
19
|
+
When targeting modern framework projects, generate:
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
export class Game {
|
|
23
|
+
constructor(bga) {
|
|
24
|
+
this.bga = bga;
|
|
25
|
+
// bga.states.register('actionPlay', new PlayState(this, bga));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setup(gamedatas) {
|
|
29
|
+
this.gamedatas = gamedatas;
|
|
30
|
+
this.bga.notifications.setupPromiseNotifications();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## State Handler Class Shape
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
export class PlayState {
|
|
39
|
+
constructor(game, bga) {
|
|
40
|
+
this.game = game;
|
|
41
|
+
this.bga = bga;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onEnteringState(args, isCurrentPlayerActive) {}
|
|
45
|
+
onLeavingState() {}
|
|
46
|
+
onPlayerActivationChange(args, isCurrentPlayerActive) {}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Action Dispatch Pattern
|
|
51
|
+
|
|
52
|
+
Use framework actions instead of ad-hoc network code:
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
this.bga.actions.performAction('actPlaySet', { card_ids: [1, 2, 3] });
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Notifications Pattern
|
|
59
|
+
|
|
60
|
+
- Call `bga.notifications.setupPromiseNotifications()` during setup.
|
|
61
|
+
- Implement handlers as `async notif_*` when they include async UI work.
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
async notif_setPlayed(args) {
|
|
65
|
+
// update local state
|
|
66
|
+
// await animation or delay if needed
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Preferences Pattern
|
|
71
|
+
|
|
72
|
+
Use `bga.userPreferences` for user settings in modern projects.
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
const value = this.bga.userPreferences.get(prefId);
|
|
76
|
+
this.bga.userPreferences.set(prefId, nextValue);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Testability Rule
|
|
80
|
+
|
|
81
|
+
Keep core gameplay calculations in pure utility functions and test with Jest.
|
|
82
|
+
|
|
83
|
+
Pattern from repo:
|
|
84
|
+
|
|
85
|
+
- `isValidSet(cards)`
|
|
86
|
+
- `calculateScore(setCount, streak)`
|
|
87
|
+
- `getLegalMoves(cards)`
|
|
88
|
+
|
|
89
|
+
Write tests using deterministic input/output only; avoid DOM/network in unit tests.
|