board-game-engine-react 1.0.2 → 2.0.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.
Files changed (56) hide show
  1. package/.storybook/webpack.config.js +1 -1
  2. package/babel.config.js +2 -1
  3. package/dist/board-game-engine-react.cjs +100 -90
  4. package/dist/board-game-engine-react.mjs +103 -78
  5. package/dist/components/abstract-choices/abstract-choices.d.ts +2 -0
  6. package/dist/components/abstract-choices/abstract-choices.d.ts.map +1 -0
  7. package/dist/components/board/grid.d.ts +5 -0
  8. package/dist/components/board/grid.d.ts.map +1 -0
  9. package/dist/components/entity/entity.d.ts +5 -0
  10. package/dist/components/entity/entity.d.ts.map +1 -0
  11. package/dist/components/game/game.d.ts +8 -0
  12. package/dist/components/game/game.d.ts.map +1 -0
  13. package/dist/components/game-status/game-status.d.ts +5 -0
  14. package/dist/components/game-status/game-status.d.ts.map +1 -0
  15. package/dist/components/space/space.d.ts +5 -0
  16. package/dist/components/space/space.d.ts.map +1 -0
  17. package/dist/contexts/game-context.d.ts +15 -0
  18. package/dist/contexts/game-context.d.ts.map +1 -0
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/types.d.ts +24 -0
  22. package/dist/types.d.ts.map +1 -0
  23. package/dist/use-gameserver-connection.d.ts +8 -0
  24. package/dist/use-gameserver-connection.d.ts.map +1 -0
  25. package/examples/checkers.json +2 -2
  26. package/examples/connect-four.json +1 -1
  27. package/examples/eights.json +15 -15
  28. package/examples/index.html +219 -0
  29. package/package.json +12 -10
  30. package/scripts/build.mjs +3 -3
  31. package/src/components/abstract-choices/{abstract-choices.js → abstract-choices.tsx} +12 -8
  32. package/src/components/board/grid.tsx +29 -0
  33. package/src/components/entity/entity.tsx +40 -0
  34. package/src/components/game/{game.js → game.tsx} +22 -9
  35. package/src/components/game-status/game-status.tsx +30 -0
  36. package/src/components/space/space.tsx +52 -0
  37. package/src/contexts/game-context.tsx +60 -0
  38. package/src/global.d.ts +1 -0
  39. package/src/index.ts +8 -0
  40. package/src/types.ts +25 -0
  41. package/src/use-gameserver-connection.ts +82 -0
  42. package/stories/BoardGameEngineReact.stories.tsx +85 -0
  43. package/test-app/index.html +1 -1
  44. package/test-app/{main.jsx → main.tsx} +13 -11
  45. package/tsconfig.build.json +14 -0
  46. package/tsconfig.json +34 -0
  47. package/board-game-engine-react.test.js +0 -12
  48. package/jest.config.js +0 -13
  49. package/src/components/board/grid.js +0 -28
  50. package/src/components/entity/entity.js +0 -38
  51. package/src/components/game-status/game-status.js +0 -20
  52. package/src/components/space/space.js +0 -68
  53. package/src/contexts/game-context.js +0 -35
  54. package/src/index.js +0 -5
  55. package/src/use-gameserver-connection.js +0 -74
  56. package/stories/BoardGameEngineReact.stories.js +0 -72
@@ -0,0 +1,219 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Board Game Engine Examples</title>
6
+ <style>
7
+ body {
8
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
9
+ margin: 0;
10
+ padding: 1rem 1.5rem 2rem;
11
+ background: #0f172a;
12
+ color: #e5e7eb;
13
+ }
14
+ h1 {
15
+ font-size: 1.4rem;
16
+ margin-bottom: 0.75rem;
17
+ }
18
+ .controls {
19
+ display: flex;
20
+ flex-wrap: wrap;
21
+ gap: 0.75rem;
22
+ align-items: center;
23
+ margin-bottom: 1rem;
24
+ }
25
+ select, input, button {
26
+ background: #020617;
27
+ color: #e5e7eb;
28
+ border-radius: 999px;
29
+ border: 1px solid #1f2937;
30
+ padding: 0.35rem 0.9rem;
31
+ font-size: 0.9rem;
32
+ outline: none;
33
+ }
34
+ select:focus, input:focus, button:focus {
35
+ border-color: #38bdf8;
36
+ box-shadow: 0 0 0 1px #38bdf8;
37
+ }
38
+ button {
39
+ cursor: pointer;
40
+ background: linear-gradient(135deg, #22c55e, #16a34a);
41
+ border: none;
42
+ color: #022c22;
43
+ font-weight: 600;
44
+ }
45
+ button:hover {
46
+ filter: brightness(1.08);
47
+ }
48
+ button:disabled {
49
+ opacity: 0.5;
50
+ cursor: default;
51
+ }
52
+ label {
53
+ font-size: 0.85rem;
54
+ opacity: 0.85;
55
+ }
56
+ .layout {
57
+ display: grid;
58
+ grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
59
+ gap: 1rem;
60
+ align-items: flex-start;
61
+ }
62
+ #debug {
63
+ background: #020617;
64
+ border-radius: 0.85rem;
65
+ border: 1px solid #1f2937;
66
+ min-height: 320px;
67
+ padding: 0.5rem;
68
+ overflow: auto;
69
+ }
70
+ .panel {
71
+ background: #020617;
72
+ border-radius: 0.85rem;
73
+ border: 1px solid #1f2937;
74
+ padding: 0.75rem 0.9rem;
75
+ min-height: 160px;
76
+ }
77
+ .panel h2 {
78
+ margin: 0 0 0.5rem;
79
+ font-size: 0.9rem;
80
+ letter-spacing: 0.06em;
81
+ text-transform: uppercase;
82
+ opacity: 0.7;
83
+ }
84
+ pre {
85
+ margin: 0;
86
+ font-size: 0.78rem;
87
+ line-height: 1.4;
88
+ white-space: pre-wrap;
89
+ word-break: break-word;
90
+ }
91
+ .badge {
92
+ display: inline-flex;
93
+ align-items: center;
94
+ gap: 0.25rem;
95
+ padding: 0.1rem 0.5rem;
96
+ border-radius: 999px;
97
+ background: rgba(56, 189, 248, 0.1);
98
+ border: 1px solid rgba(56, 189, 248, 0.5);
99
+ font-size: 0.7rem;
100
+ text-transform: uppercase;
101
+ letter-spacing: 0.08em;
102
+ }
103
+ </style>
104
+ </head>
105
+ <body>
106
+ <h1>Board Game Engine – Examples</h1>
107
+ <div class="controls">
108
+ <label>
109
+ Game&nbsp;
110
+ <select id="game-select">
111
+ <option value="tic-tac-toe">Tic Tac Toe</option>
112
+ <option value="connect-four">Connect Four</option>
113
+ <option value="reversi">Reversi</option>
114
+ <option value="eights">Crazy Eights</option>
115
+ <option value="checkers">Checkers</option>
116
+ </select>
117
+ </label>
118
+ <label>
119
+ Players&nbsp;
120
+ <input id="players-input" type="number" min="1" max="6" value="2" />
121
+ </label>
122
+ <button id="start-btn">Start / Reset Game</button>
123
+ <span class="badge">Debug UI attaches below</span>
124
+ </div>
125
+
126
+ <div class="layout">
127
+ <div id="debug"></div>
128
+ <div class="panel">
129
+ <h2>Client snapshot</h2>
130
+ <pre id="state-view">(no game started)</pre>
131
+ </div>
132
+ </div>
133
+
134
+ <script src="../dist/board-game-engine.js"></script>
135
+ <script>
136
+ (function () {
137
+ const gameFiles = {
138
+ 'tic-tac-toe': './tic-tac-toe.json',
139
+ 'connect-four': './connect-four.json',
140
+ 'reversi': './reversi.json',
141
+ 'eights': './eights.json',
142
+ 'checkers': './checkers.json',
143
+ };
144
+
145
+ const selectEl = document.getElementById('game-select');
146
+ const playersEl = document.getElementById('players-input');
147
+ const startBtn = document.getElementById('start-btn');
148
+ const stateView = document.getElementById('state-view');
149
+
150
+ let currentClient = null;
151
+
152
+ function toSerializableSnapshot(snapshot) {
153
+ // Avoid circular structures by swapping in originalG (plain JSON)
154
+ if (!snapshot || !snapshot.state) return snapshot;
155
+ const { state, ...rest } = snapshot;
156
+ const { originalG, G, ...stateRest } = state;
157
+ return {
158
+ ...rest,
159
+ state: {
160
+ ...stateRest,
161
+ // originalG is what came from boardgame.io before our deserialization step
162
+ G: originalG ?? G,
163
+ },
164
+ };
165
+ }
166
+
167
+ async function startGame() {
168
+ const key = selectEl.value;
169
+ const file = gameFiles[key];
170
+ if (!file || !window.BoardGameEngine || !window.BoardGameEngine.Client) {
171
+ stateView.textContent = 'Engine bundle or game config not available.';
172
+ return;
173
+ }
174
+
175
+ startBtn.disabled = true;
176
+ stateView.textContent = 'Loading ' + key + '…';
177
+
178
+ try {
179
+ const res = await fetch(file);
180
+ const rules = await res.json();
181
+
182
+ const numPlayers =
183
+ Number(playersEl.value) ||
184
+ rules.numPlayers ||
185
+ rules.maxPlayers ||
186
+ 2;
187
+
188
+ const client = new window.BoardGameEngine.Client({
189
+ gameRules: JSON.stringify(rules),
190
+ gameName: key,
191
+ numPlayers,
192
+ onClientUpdate: () => {
193
+ const snapshot = client.getState();
194
+ const safe = toSerializableSnapshot(snapshot);
195
+ stateView.textContent = JSON.stringify(safe, null, 2);
196
+ },
197
+ });
198
+
199
+ currentClient = client.connect();
200
+ if (currentClient && typeof currentClient.getState === 'function') {
201
+ // Force initial snapshot after connect
202
+ const snapshot = currentClient.getState();
203
+ const safe = toSerializableSnapshot(snapshot);
204
+ stateView.textContent = JSON.stringify(safe, null, 2);
205
+ }
206
+ } catch (err) {
207
+ console.error(err);
208
+ stateView.textContent = 'Error loading game: ' + (err && err.message ? err.message : String(err));
209
+ } finally {
210
+ startBtn.disabled = false;
211
+ }
212
+ }
213
+
214
+ startBtn.addEventListener('click', startGame);
215
+ })();
216
+ </script>
217
+ </body>
218
+ </html>
219
+
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "board-game-engine-react",
3
- "version": "1.0.2",
3
+ "version": "2.0.0",
4
4
  "description": "React library for using board-game-engine",
5
5
  "main": "./dist/board-game-engine-react.cjs",
6
6
  "module": "./dist/board-game-engine-react.mjs",
7
+ "types": "./dist/index.d.ts",
7
8
  "style": "./dist/board-game-engine-react.css",
8
9
  "exports": {
9
10
  ".": {
11
+ "types": "./dist/index.d.ts",
10
12
  "import": "./dist/board-game-engine-react.mjs",
11
13
  "require": "./dist/board-game-engine-react.cjs"
12
14
  },
@@ -14,9 +16,10 @@
14
16
  },
15
17
  "scripts": {
16
18
  "start": "storybook dev p 6006",
17
- "test": "jest --verbose false",
18
- "test:coverage": "jest --coverage --verbose false",
19
- "build": "node scripts/build.mjs",
19
+ "test": "playwright test",
20
+ "build": "node scripts/build.mjs && npm run build:types",
21
+ "build:types": "tsc -p tsconfig.build.json",
22
+ "typecheck": "tsc --noEmit",
20
23
  "build:coverage": "cross-env COVERAGE=1 node scripts/build.mjs",
21
24
  "storybook": "storybook dev -p 6006",
22
25
  "prepare": "npm run build",
@@ -48,6 +51,7 @@
48
51
  "@babel/plugin-proposal-object-rest-spread": "",
49
52
  "@babel/preset-env": "^7.26.0",
50
53
  "@babel/preset-react": "",
54
+ "@babel/preset-typescript": "^7.26.0",
51
55
  "@chromatic-com/storybook": "^3.2.2",
52
56
  "@playwright/test": "^1.49.0",
53
57
  "@storybook/addon-essentials": "^8.4.6",
@@ -59,12 +63,10 @@
59
63
  "@storybook/react": "^8.4.6",
60
64
  "@storybook/react-webpack5": "^8.4.6",
61
65
  "@storybook/test": "^8.4.6",
62
- "@testing-library/jest-dom": "^5.16.1",
63
- "@testing-library/react": "^13.4.0",
64
- "@testing-library/user-event": "^13.5.0",
66
+ "@types/react": "^18.2.0",
67
+ "@types/react-dom": "^18.2.0",
65
68
  "@vitejs/plugin-react": "^4.2.1",
66
69
  "assert": "^2.0.0",
67
- "babel-jest": "^27.4.4",
68
70
  "babel-loader": "^8.3.0",
69
71
  "babel-plugin-istanbul": "^6.1.1",
70
72
  "cross-env": "^7.0.3",
@@ -78,7 +80,6 @@
78
80
  "eslint-plugin-react": "",
79
81
  "eslint-plugin-storybook": "^0.11.1",
80
82
  "install": "^0.13.0",
81
- "jest": "^27.4.3",
82
83
  "markdown-to-jsx": "",
83
84
  "node-sass": "^7.0.1",
84
85
  "npm": "^8.3.1",
@@ -91,10 +92,11 @@
91
92
  "sass-loader": "^12.4.0",
92
93
  "source-loader": "",
93
94
  "storybook": "^8.4.6",
95
+ "typescript": "^5.9.3",
94
96
  "vite": "^5.0.0"
95
97
  },
96
98
  "dependencies": {
97
- "board-game-engine": "^1.0.5",
99
+ "board-game-engine": "^2.1.0",
98
100
  "react": "^18.2.0",
99
101
  "react-error-boundary": "^3.1.4",
100
102
  "react-transition-group": "^4.4.5"
package/scripts/build.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as esbuild from 'esbuild'
2
2
 
3
- const entry = 'src/index.js'
3
+ const entry = 'src/index.ts'
4
4
  const outdir = 'dist'
5
5
  const coverage = process.env.COVERAGE === '1'
6
6
 
@@ -10,7 +10,7 @@ const base = {
10
10
  platform: 'browser',
11
11
  target: ['es2020'],
12
12
  jsx: 'automatic',
13
- loader: { '.js': 'jsx' },
13
+ loader: { '.ts': 'tsx', '.tsx': 'tsx' },
14
14
  external: ['react', 'react-dom', 'board-game-engine'],
15
15
  }
16
16
 
@@ -25,7 +25,7 @@ if (coverage) {
25
25
  esbuildPluginIstanbul({
26
26
  name: 'istanbul',
27
27
  filter: /\.(js|ts|jsx|tsx)$/,
28
- include: ['src/**/*.js'],
28
+ include: ['src/**/*.ts', 'src/**/*.tsx'],
29
29
  exclude: ['**/*.spec.js', '**/node_modules/**'],
30
30
  }),
31
31
  ],
@@ -1,18 +1,19 @@
1
- import React from 'react'
2
- import { useGame } from "../../contexts/game-context.js";
1
+ import { useGame } from '../../contexts/game-context'
2
+ import type { GameEntity } from '../../types'
3
3
 
4
4
  export default function AbstractChoices () {
5
5
  const { clickTarget, allClickable, undoStep, currentMoveTargets } = useGame()
6
6
 
7
- const abstractChoices = [...allClickable].filter(c => c.abstract)
7
+ const abstractChoices = [...allClickable].filter(
8
+ (c) => (c as GameEntity).abstract
9
+ ) as GameEntity[]
8
10
 
9
- // spacer assumes only one row of choices.
10
- // could save and store biggest height instead?
11
11
  return (
12
12
  <div style={{ position: 'relative' }}>
13
13
  <button
14
- style={{visibility: 'hidden'}}
14
+ style={{ visibility: 'hidden' }}
15
15
  className="button button--style-b button--x-small abstract-choices__choice"
16
+ type="button"
16
17
  >
17
18
  Spacer
18
19
  </button>
@@ -21,9 +22,11 @@ export default function AbstractChoices () {
21
22
  position: 'absolute',
22
23
  top: 0,
23
24
  width: '100%',
24
- }}>
25
+ }}
26
+ >
25
27
  {!!currentMoveTargets.length && (
26
28
  <button
29
+ type="button"
27
30
  className="button button--style-c button--x-small abstract-choices__choice abstract-choices__choice--undo"
28
31
  onClick={undoStep}
29
32
  >
@@ -33,10 +36,11 @@ export default function AbstractChoices () {
33
36
  {abstractChoices.map((choice, i) => (
34
37
  <button
35
38
  key={i}
39
+ type="button"
36
40
  className="button button--style-b button--x-small abstract-choices__choice"
37
41
  onClick={() => clickTarget(choice)}
38
42
  >
39
- {choice.value}
43
+ {String(choice.value ?? '')}
40
44
  </button>
41
45
  ))}
42
46
  </div>
@@ -0,0 +1,29 @@
1
+ import Entity from '../entity/entity'
2
+ import type { GameEntity } from '../../types'
3
+
4
+ export default function Grid ({ grid }: { grid: GameEntity }) {
5
+ const { width, height, spaces } = grid.attributes
6
+ const w = Number(width ?? 0)
7
+ const h = Number(height ?? 0)
8
+ const spaceList = (spaces ?? []) as GameEntity[]
9
+ return (
10
+ <div
11
+ className="grid"
12
+ style={{
13
+ display: 'inline-grid',
14
+ width: '100%',
15
+ gridTemplateColumns: `repeat(${w}, 1fr)`,
16
+ gridTemplateRows: `repeat(${h}, 1fr)`,
17
+ }}
18
+ >
19
+ {spaceList.map((space, index) => (
20
+ <div
21
+ key={index}
22
+ className="grid__cell"
23
+ >
24
+ <Entity entity={space} />
25
+ </div>
26
+ ))}
27
+ </div>
28
+ )
29
+ }
@@ -0,0 +1,40 @@
1
+ import { useGame } from '../../contexts/game-context'
2
+ import Grid from '../board/grid'
3
+ import Space from '../space/space'
4
+ import type { GameEntity } from '../../types'
5
+
6
+ export default function Entity ({ entity }: { entity: GameEntity }) {
7
+ const { clickTarget, allClickable } = useGame()
8
+ const isClickable = allClickable.has(entity)
9
+ const attributes = entity.attributes
10
+ const entityType = attributes.entityType ?? attributes.type
11
+
12
+ switch (entityType) {
13
+ case 'Grid':
14
+ return <Grid grid={entity} />
15
+ case 'Space':
16
+ return <Space space={entity} />
17
+ default:
18
+ return (
19
+ <div
20
+ onClick={(e) => {
21
+ if (isClickable) {
22
+ e.stopPropagation()
23
+ clickTarget(entity)
24
+ }
25
+ }}
26
+ className={[
27
+ 'entity',
28
+ attributes.player !== undefined && `player-${String(attributes.player)}`,
29
+ isClickable && 'entity--clickable',
30
+ ].filter(Boolean).join(' ')}
31
+ >
32
+ {entity.rule.displayProperties?.map((property, i) => (
33
+ <div key={i}>
34
+ {property}: {String((attributes[property] as string | number | boolean | undefined) ?? '')}
35
+ </div>
36
+ ))}
37
+ </div>
38
+ )
39
+ }
40
+ }
@@ -1,16 +1,29 @@
1
- import React from 'react'
2
- import Entity from '../entity/entity.js'
3
- import AbstractChoices from '../abstract-choices/abstract-choices.js'
4
- import GameStatus from '../game-status/game-status.js'
5
- import { GameProvider } from "../../contexts/game-context.js";
1
+ import type { ReactNode } from 'react'
2
+ import Entity from '../entity/entity'
3
+ import AbstractChoices from '../abstract-choices/abstract-choices'
4
+ import GameStatus from '../game-status/game-status'
5
+ import { GameProvider } from '../../contexts/game-context'
6
+ import type { GameEntity, GameserverConnection } from '../../types'
6
7
 
7
- export default function Game ({ gameConnection, loading, isSpectator }) {
8
- const G = gameConnection?.state?.G
8
+ export default function Game ({
9
+ gameConnection,
10
+ loading,
11
+ isSpectator,
12
+ }: {
13
+ gameConnection?: GameserverConnection | Record<string, never> | null
14
+ loading?: ReactNode
15
+ isSpectator?: boolean
16
+ }) {
17
+ const G = gameConnection && 'state' in gameConnection
18
+ ? (gameConnection as unknown as GameserverConnection).state?.G as
19
+ | { sharedBoard: { entities: GameEntity[] }; personalBoards?: { entities: GameEntity[] }[] }
20
+ | undefined
21
+ : undefined
9
22
 
10
23
  return G
11
24
  ? (
12
25
  <GameProvider
13
- gameConnection={gameConnection}
26
+ gameConnection={gameConnection as GameserverConnection}
14
27
  isSpectator={isSpectator}
15
28
  >
16
29
  <div className="game">
@@ -49,7 +62,7 @@ export default function Game ({ gameConnection, loading, isSpectator }) {
49
62
  ))}
50
63
  </div>
51
64
  )}
52
- <GameStatus gameConnection={gameConnection} />
65
+ <GameStatus gameConnection={gameConnection as GameserverConnection} />
53
66
  </div>
54
67
  </GameProvider>
55
68
  )
@@ -0,0 +1,30 @@
1
+ import type { GameserverConnection } from '../../types'
2
+
3
+ export default function GameStatus ({
4
+ gameConnection,
5
+ }: {
6
+ gameConnection: GameserverConnection
7
+ }) {
8
+ const state = gameConnection.state
9
+ if (!state) return null
10
+
11
+ const players = gameConnection.client?.matchData as
12
+ | Record<string, { name?: string }>
13
+ | undefined
14
+ const winner = state.ctx.gameover?.winner as string | number | undefined
15
+ const draw = state.ctx.gameover?.draw
16
+ let winnerString = ''
17
+ if (draw) {
18
+ winnerString = 'Draw!'
19
+ } else if (players != null && winner != null && winner !== '') {
20
+ const key = String(winner)
21
+ winnerString = `${players[key]?.name ?? ''} Wins!`
22
+ } else if (winner != null && winner !== '') {
23
+ winnerString = `Player ${String(winner)} Wins!`
24
+ }
25
+ return state.ctx.gameover && (
26
+ <div className="game-status">
27
+ {winnerString}
28
+ </div>
29
+ )
30
+ }
@@ -0,0 +1,52 @@
1
+ import { useGame } from '../../contexts/game-context'
2
+ import Entity from '../entity/entity'
3
+ import type { GameEntity } from '../../types'
4
+
5
+ export default function Space ({ space }: { space: GameEntity }) {
6
+ const { clickTarget, allClickable, currentMoveTargets } = useGame()
7
+ const { entities, entityId } = space.attributes
8
+
9
+ const entityList = entities ?? []
10
+ const clickable = allClickable.has(space)
11
+ const targeted = currentMoveTargets
12
+ .map((e) => (e as GameEntity).entityId ?? (e as GameEntity).attributes?.entityId)
13
+ .includes(entityId)
14
+
15
+ return (
16
+ <a
17
+ className={[
18
+ 'space',
19
+ clickable && 'space--clickable',
20
+ targeted && 'space--targeted',
21
+ ].filter(Boolean).join(' ')}
22
+ onClick={() => clickTarget(space)}
23
+ style={{
24
+ display: 'inline-block',
25
+ flex: '1',
26
+ }}
27
+ >
28
+ <div
29
+ className="space__entity-grid"
30
+ style={{
31
+ display: 'flex',
32
+ height: '100%',
33
+ width: '100%',
34
+ flexWrap: 'wrap',
35
+ }}
36
+ >
37
+ {Array.from({ length: entityList.length }, (_, i) => (
38
+ <div
39
+ className="space__entity-grid__cell"
40
+ style={{
41
+ display: 'inline-block',
42
+ }}
43
+ key={i}
44
+ >
45
+ <Entity entity={entityList[i]} />
46
+ </div>
47
+ ))}
48
+ {!entityList.length && String(space.attributes.name ?? '')}
49
+ </div>
50
+ </a>
51
+ )
52
+ }
@@ -0,0 +1,60 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useEffect,
5
+ type ReactNode,
6
+ } from 'react'
7
+ import type { GameserverConnection } from '../types'
8
+
9
+ export interface GameContextValue {
10
+ clickTarget: (target: unknown) => void
11
+ undoStep: () => void
12
+ allClickable: Set<unknown>
13
+ currentMoveTargets: unknown[]
14
+ }
15
+
16
+ const GameContext = createContext<GameContextValue>({
17
+ clickTarget: () => {},
18
+ undoStep: () => {},
19
+ allClickable: new Set(),
20
+ currentMoveTargets: [],
21
+ })
22
+
23
+ export function GameProvider ({
24
+ gameConnection,
25
+ children,
26
+ isSpectator,
27
+ }: {
28
+ gameConnection: GameserverConnection
29
+ children: ReactNode
30
+ isSpectator?: boolean
31
+ }) {
32
+ useEffect(() => {
33
+ if (gameConnection.state?._stateID === 0) {
34
+ gameConnection.reset()
35
+ }
36
+ }, [gameConnection.state?._stateID])
37
+
38
+ return (
39
+ <GameContext.Provider
40
+ value={{
41
+ clickTarget: (target: unknown) => {
42
+ if (!isSpectator) {
43
+ gameConnection.doStep(target)
44
+ }
45
+ },
46
+ undoStep: () => { gameConnection.undoStep() },
47
+ allClickable: (gameConnection.optimisticWinner || isSpectator)
48
+ ? new Set()
49
+ : (gameConnection.allClickable ?? new Set()),
50
+ currentMoveTargets: (gameConnection.optimisticWinner || isSpectator)
51
+ ? []
52
+ : (gameConnection.moveBuilder?.targets ?? []),
53
+ }}
54
+ >
55
+ {children}
56
+ </GameContext.Provider>
57
+ )
58
+ }
59
+
60
+ export const useGame = () => useContext(GameContext)
@@ -0,0 +1 @@
1
+ declare module '*.css' {}
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ import './styles.css'
2
+ export { Client, gameFactory } from 'board-game-engine'
3
+ export type { ClientOptions, GameFactoryInput } from 'board-game-engine'
4
+
5
+ export { default as Game } from './components/game/game'
6
+ export { useGameserverConnection } from './use-gameserver-connection'
7
+ export type { UseGameserverConnectionParams } from './use-gameserver-connection'
8
+ export type { GameserverConnection } from './types'
package/src/types.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { Client } from 'board-game-engine'
2
+
3
+ export type GameserverConnection = Client & ReturnType<Client['getState']>
4
+
5
+ /** Bagel / board entity passed through the React tree */
6
+ export interface GameEntity {
7
+ attributes: {
8
+ entityType?: string
9
+ type?: string
10
+ player?: string | number
11
+ width?: number
12
+ height?: number
13
+ spaces?: GameEntity[]
14
+ entities?: GameEntity[]
15
+ entityId?: unknown
16
+ name?: string
17
+ [key: string]: unknown
18
+ }
19
+ rule: {
20
+ displayProperties?: string[]
21
+ }
22
+ entityId?: unknown
23
+ abstract?: boolean
24
+ value?: unknown
25
+ }