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.
@@ -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
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ testEnvironment: 'node',
3
+ setupFiles: ['./harness/js/bgaStubs.js'],
4
+ testMatch: ['**/*.test.js']
5
+ };
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "bga-dev-skill",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "scripts": {
6
+ "test": "jest"
7
+ },
8
+ "devDependencies": {
9
+ "jest": "^29.0.0"
10
+ }
11
+ }
@@ -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.