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.
- package/.storybook/webpack.config.js +1 -1
- package/babel.config.js +2 -1
- package/dist/board-game-engine-react.cjs +100 -90
- package/dist/board-game-engine-react.mjs +103 -78
- package/dist/components/abstract-choices/abstract-choices.d.ts +2 -0
- package/dist/components/abstract-choices/abstract-choices.d.ts.map +1 -0
- package/dist/components/board/grid.d.ts +5 -0
- package/dist/components/board/grid.d.ts.map +1 -0
- package/dist/components/entity/entity.d.ts +5 -0
- package/dist/components/entity/entity.d.ts.map +1 -0
- package/dist/components/game/game.d.ts +8 -0
- package/dist/components/game/game.d.ts.map +1 -0
- package/dist/components/game-status/game-status.d.ts +5 -0
- package/dist/components/game-status/game-status.d.ts.map +1 -0
- package/dist/components/space/space.d.ts +5 -0
- package/dist/components/space/space.d.ts.map +1 -0
- package/dist/contexts/game-context.d.ts +15 -0
- package/dist/contexts/game-context.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/use-gameserver-connection.d.ts +8 -0
- package/dist/use-gameserver-connection.d.ts.map +1 -0
- package/examples/checkers.json +2 -2
- package/examples/connect-four.json +1 -1
- package/examples/eights.json +15 -15
- package/examples/index.html +219 -0
- package/package.json +12 -10
- package/scripts/build.mjs +3 -3
- package/src/components/abstract-choices/{abstract-choices.js → abstract-choices.tsx} +12 -8
- package/src/components/board/grid.tsx +29 -0
- package/src/components/entity/entity.tsx +40 -0
- package/src/components/game/{game.js → game.tsx} +22 -9
- package/src/components/game-status/game-status.tsx +30 -0
- package/src/components/space/space.tsx +52 -0
- package/src/contexts/game-context.tsx +60 -0
- package/src/global.d.ts +1 -0
- package/src/index.ts +8 -0
- package/src/types.ts +25 -0
- package/src/use-gameserver-connection.ts +82 -0
- package/stories/BoardGameEngineReact.stories.tsx +85 -0
- package/test-app/index.html +1 -1
- package/test-app/{main.jsx → main.tsx} +13 -11
- package/tsconfig.build.json +14 -0
- package/tsconfig.json +34 -0
- package/board-game-engine-react.test.js +0 -12
- package/jest.config.js +0 -13
- package/src/components/board/grid.js +0 -28
- package/src/components/entity/entity.js +0 -38
- package/src/components/game-status/game-status.js +0 -20
- package/src/components/space/space.js +0 -68
- package/src/contexts/game-context.js +0 -35
- package/src/index.js +0 -5
- package/src/use-gameserver-connection.js +0 -74
- package/stories/BoardGameEngineReact.stories.js +0 -72
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useEffect, useReducer, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Client,
|
|
4
|
+
type ClientOptions,
|
|
5
|
+
type GameFactoryInput,
|
|
6
|
+
} from 'board-game-engine'
|
|
7
|
+
import type { GameserverConnection } from './types'
|
|
8
|
+
|
|
9
|
+
export type UseGameserverConnectionParams = Omit<ClientOptions, 'gameRules'> & {
|
|
10
|
+
gameRules?: GameFactoryInput
|
|
11
|
+
enabled?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useGameserverConnection ({
|
|
15
|
+
server,
|
|
16
|
+
multiplayer,
|
|
17
|
+
matchID,
|
|
18
|
+
gameRules,
|
|
19
|
+
gameName,
|
|
20
|
+
boardgameIOGame,
|
|
21
|
+
playerID,
|
|
22
|
+
credentials,
|
|
23
|
+
numPlayers,
|
|
24
|
+
debug,
|
|
25
|
+
enabled = true,
|
|
26
|
+
}: UseGameserverConnectionParams): GameserverConnection | Record<string, never> {
|
|
27
|
+
const [_tick, forceUpdate] = useReducer((x: number) => x + 1, 0)
|
|
28
|
+
const [connection, setConnection] = useState<Client | null>(null)
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (
|
|
32
|
+
(!gameRules && !boardgameIOGame) ||
|
|
33
|
+
(credentials && !(matchID && enabled && server))
|
|
34
|
+
) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const options: ClientOptions = {
|
|
39
|
+
server,
|
|
40
|
+
numPlayers,
|
|
41
|
+
onClientUpdate: () => {
|
|
42
|
+
forceUpdate()
|
|
43
|
+
},
|
|
44
|
+
debug,
|
|
45
|
+
matchID,
|
|
46
|
+
gameRules,
|
|
47
|
+
boardgameIOGame,
|
|
48
|
+
gameName,
|
|
49
|
+
playerID,
|
|
50
|
+
credentials,
|
|
51
|
+
multiplayer,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const newConnection = new Client(options)
|
|
55
|
+
|
|
56
|
+
newConnection.connect()
|
|
57
|
+
|
|
58
|
+
setConnection(newConnection)
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
newConnection.client?.stop()
|
|
62
|
+
setConnection(null)
|
|
63
|
+
}
|
|
64
|
+
}, [
|
|
65
|
+
matchID,
|
|
66
|
+
server,
|
|
67
|
+
playerID,
|
|
68
|
+
credentials,
|
|
69
|
+
gameRules,
|
|
70
|
+
boardgameIOGame,
|
|
71
|
+
enabled,
|
|
72
|
+
multiplayer,
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
if (connection) {
|
|
76
|
+
return Object.assign(
|
|
77
|
+
connection,
|
|
78
|
+
connection.getState?.(),
|
|
79
|
+
) as GameserverConnection
|
|
80
|
+
}
|
|
81
|
+
return {}
|
|
82
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import type { GameFactoryInput } from 'board-game-engine'
|
|
3
|
+
import { Game, useGameserverConnection } from '../src/index'
|
|
4
|
+
import ticTacToe from './tic-tac-toe.json'
|
|
5
|
+
import { ErrorBoundary } from 'react-error-boundary'
|
|
6
|
+
import './styles.css'
|
|
7
|
+
|
|
8
|
+
const sampleGame = ticTacToe as unknown as GameFactoryInput
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
title: 'Game',
|
|
12
|
+
component: Game,
|
|
13
|
+
className: 'board-game-engine-react',
|
|
14
|
+
args: {
|
|
15
|
+
gameRules: sampleGame,
|
|
16
|
+
numPlayers: 2,
|
|
17
|
+
isSpectator: false,
|
|
18
|
+
},
|
|
19
|
+
argTypes: {
|
|
20
|
+
gameRules: { control: 'object' },
|
|
21
|
+
numPlayers: { control: 'number' },
|
|
22
|
+
isSpectator: { control: 'boolean' },
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const fallbackRender = ({ error }: { error: Error }) => <div>Invalid Game Rules {error.message}</div>
|
|
27
|
+
|
|
28
|
+
const Template = ({
|
|
29
|
+
gameRules,
|
|
30
|
+
numPlayers,
|
|
31
|
+
loading,
|
|
32
|
+
isSpectator,
|
|
33
|
+
}: {
|
|
34
|
+
gameRules: GameFactoryInput
|
|
35
|
+
numPlayers: number
|
|
36
|
+
loading?: ReactNode
|
|
37
|
+
isSpectator?: boolean
|
|
38
|
+
}) => {
|
|
39
|
+
const gameConnection = useGameserverConnection({
|
|
40
|
+
gameRules,
|
|
41
|
+
numPlayers,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="story">
|
|
46
|
+
<ErrorBoundary
|
|
47
|
+
resetKeys={[JSON.stringify(gameRules), numPlayers]}
|
|
48
|
+
fallbackRender={fallbackRender}
|
|
49
|
+
>
|
|
50
|
+
<Game
|
|
51
|
+
gameConnection={gameConnection}
|
|
52
|
+
loading={loading}
|
|
53
|
+
isSpectator={isSpectator}
|
|
54
|
+
/>
|
|
55
|
+
</ErrorBoundary>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const Basic = Template.bind({})
|
|
61
|
+
|
|
62
|
+
type TemplateProps = Parameters<typeof Template>[0]
|
|
63
|
+
|
|
64
|
+
export const WithLoading = Object.assign(
|
|
65
|
+
(args: TemplateProps) => (
|
|
66
|
+
<Template
|
|
67
|
+
{...args}
|
|
68
|
+
loading={<div className="story-loading">Loading game…</div>}
|
|
69
|
+
/>
|
|
70
|
+
),
|
|
71
|
+
{
|
|
72
|
+
args: {
|
|
73
|
+
gameRules: sampleGame,
|
|
74
|
+
numPlayers: 2,
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
export const Spectator = Object.assign(Template.bind({}), {
|
|
80
|
+
args: {
|
|
81
|
+
gameRules: sampleGame,
|
|
82
|
+
numPlayers: 2,
|
|
83
|
+
isSpectator: true,
|
|
84
|
+
},
|
|
85
|
+
})
|
package/test-app/index.html
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState, useCallback } from 'react'
|
|
2
2
|
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import type { GameFactoryInput } from 'board-game-engine'
|
|
3
4
|
import '../dist/board-game-engine-react.css'
|
|
4
5
|
import { Game, useGameserverConnection } from 'board-game-engine-react'
|
|
5
6
|
|
|
@@ -14,9 +15,9 @@ const GAME_OPTIONS = [
|
|
|
14
15
|
function Harness () {
|
|
15
16
|
const [gameKey, setGameKey] = useState('tic-tac-toe')
|
|
16
17
|
const [numPlayers, setNumPlayers] = useState(2)
|
|
17
|
-
const [gameRules, setGameRules] = useState(null)
|
|
18
|
+
const [gameRules, setGameRules] = useState<GameFactoryInput | null>(null)
|
|
18
19
|
const [loading, setLoading] = useState(false)
|
|
19
|
-
const [error, setError] = useState(null)
|
|
20
|
+
const [error, setError] = useState<string | null>(null)
|
|
20
21
|
|
|
21
22
|
const startGame = useCallback(async () => {
|
|
22
23
|
setError(null)
|
|
@@ -24,12 +25,12 @@ function Harness () {
|
|
|
24
25
|
try {
|
|
25
26
|
const res = await fetch(`/games/${gameKey}.json`)
|
|
26
27
|
if (!res.ok) throw new Error(res.statusText)
|
|
27
|
-
const rules = await res.json()
|
|
28
|
+
const rules = await res.json() as GameFactoryInput
|
|
28
29
|
const n = Number(numPlayers) || rules.numPlayers || rules.maxPlayers || 2
|
|
29
|
-
setGameRules(
|
|
30
|
+
setGameRules(rules)
|
|
30
31
|
setNumPlayers(n)
|
|
31
32
|
} catch (err) {
|
|
32
|
-
setError(err.message
|
|
33
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
33
34
|
setGameRules(null)
|
|
34
35
|
} finally {
|
|
35
36
|
setLoading(false)
|
|
@@ -37,14 +38,13 @@ function Harness () {
|
|
|
37
38
|
}, [gameKey, numPlayers])
|
|
38
39
|
|
|
39
40
|
const connection = useGameserverConnection({
|
|
40
|
-
|
|
41
|
-
gameRules: gameRules || '',
|
|
41
|
+
gameRules: gameRules ?? undefined,
|
|
42
42
|
gameName: gameKey,
|
|
43
43
|
numPlayers,
|
|
44
|
-
enabled:
|
|
44
|
+
enabled: gameRules != null,
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
-
const hasState = connection
|
|
47
|
+
const hasState = connection && 'getState' in connection ? connection.getState?.() : undefined
|
|
48
48
|
|
|
49
49
|
return (
|
|
50
50
|
<div className="harness" data-testid="harness">
|
|
@@ -98,4 +98,6 @@ function Harness () {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
const root = document.getElementById('root')
|
|
101
|
-
|
|
101
|
+
if (root) {
|
|
102
|
+
createRoot(root).render(<Harness />)
|
|
103
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": false,
|
|
5
|
+
"emitDeclarationOnly": true,
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"rootDir": "src",
|
|
10
|
+
"stripInternal": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts"],
|
|
13
|
+
"exclude": ["node_modules", "dist"]
|
|
14
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": ".",
|
|
4
|
+
"paths": {
|
|
5
|
+
"board-game-engine-react": ["./src/index.ts"]
|
|
6
|
+
},
|
|
7
|
+
"target": "ES2020",
|
|
8
|
+
"module": "ESNext",
|
|
9
|
+
"moduleResolution": "Bundler",
|
|
10
|
+
"lib": ["ES2020", "DOM"],
|
|
11
|
+
"jsx": "react-jsx",
|
|
12
|
+
"strict": true,
|
|
13
|
+
"noImplicitAny": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"allowJs": false,
|
|
17
|
+
"resolveJsonModule": true,
|
|
18
|
+
"isolatedModules": true,
|
|
19
|
+
"verbatimModuleSyntax": true,
|
|
20
|
+
"noUnusedLocals": true,
|
|
21
|
+
"noUnusedParameters": true,
|
|
22
|
+
"rootDir": "."
|
|
23
|
+
},
|
|
24
|
+
"include": [
|
|
25
|
+
"src/**/*.ts",
|
|
26
|
+
"src/**/*.tsx",
|
|
27
|
+
"src/**/*.d.ts",
|
|
28
|
+
"stories/**/*.ts",
|
|
29
|
+
"stories/**/*.tsx",
|
|
30
|
+
"test-app/**/*.tsx",
|
|
31
|
+
"board-game-engine-react.test.ts"
|
|
32
|
+
],
|
|
33
|
+
"exclude": ["node_modules", "dist"]
|
|
34
|
+
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/* eslint-env jest */
|
|
2
|
-
|
|
3
|
-
import '@testing-library/jest-dom/extend-expect'
|
|
4
|
-
import React from 'react'
|
|
5
|
-
import { render, screen } from '@testing-library/react'
|
|
6
|
-
import userEvent from '@testing-library/user-event'
|
|
7
|
-
|
|
8
|
-
describe('board-game-engine-react', () => {
|
|
9
|
-
test('has a test', async () => {
|
|
10
|
-
return true
|
|
11
|
-
})
|
|
12
|
-
})
|
package/jest.config.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
transform: {
|
|
3
|
-
'^.+\\.(js|jsx)$': 'babel-jest'
|
|
4
|
-
},
|
|
5
|
-
testEnvironment: 'jsdom',
|
|
6
|
-
transformIgnorePatterns: [
|
|
7
|
-
'/node_modules/(?!yarn-bound|@mnbroatch|react-dialogue-tree).+\\.js$'
|
|
8
|
-
],
|
|
9
|
-
moduleNameMapper: {
|
|
10
|
-
'^.+\\.(css|less|scss)$': 'babel-jest'
|
|
11
|
-
},
|
|
12
|
-
clearMocks: true
|
|
13
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import Entity from "../entity/entity.js";
|
|
3
|
-
|
|
4
|
-
export default function Grid ({ grid }) {
|
|
5
|
-
const { width, height, spaces } = grid.attributes;
|
|
6
|
-
return (
|
|
7
|
-
<div
|
|
8
|
-
className="grid"
|
|
9
|
-
style={{
|
|
10
|
-
display: 'inline-grid',
|
|
11
|
-
width: '100%',
|
|
12
|
-
gridTemplateColumns: `repeat(${width}, 1fr)`,
|
|
13
|
-
gridTemplateRows: `repeat(${height}, 1fr)`,
|
|
14
|
-
}}
|
|
15
|
-
>
|
|
16
|
-
{spaces.map((space, index) => {
|
|
17
|
-
return (
|
|
18
|
-
<div
|
|
19
|
-
key={index}
|
|
20
|
-
className="grid__cell"
|
|
21
|
-
>
|
|
22
|
-
<Entity entity={space} />
|
|
23
|
-
</div>
|
|
24
|
-
);
|
|
25
|
-
})}
|
|
26
|
-
</div>
|
|
27
|
-
);
|
|
28
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { useGame } from "../../contexts/game-context.js";
|
|
3
|
-
import Grid from '../board/grid.js'
|
|
4
|
-
import Space from "../space/space.js";
|
|
5
|
-
|
|
6
|
-
export default function Entity ({ entity }) {
|
|
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} isClickable={isClickable} />
|
|
15
|
-
case 'Space':
|
|
16
|
-
return <Space space={entity} isClickable={isClickable} />
|
|
17
|
-
default:
|
|
18
|
-
return <div
|
|
19
|
-
onClick={(e) => {
|
|
20
|
-
if (isClickable) {
|
|
21
|
-
e.stopPropagation()
|
|
22
|
-
clickTarget(entity)
|
|
23
|
-
}
|
|
24
|
-
}}
|
|
25
|
-
className={[
|
|
26
|
-
'entity',
|
|
27
|
-
attributes.player && `player-${attributes.player}`,
|
|
28
|
-
isClickable && 'entity--clickable',
|
|
29
|
-
].filter(Boolean).join(' ')}
|
|
30
|
-
>
|
|
31
|
-
{entity.rule.displayProperties?.map((property, i) => (
|
|
32
|
-
<div key={i}>
|
|
33
|
-
{property}: {(entity.attributes[property])?.toString()}
|
|
34
|
-
</div>
|
|
35
|
-
))}
|
|
36
|
-
</div>
|
|
37
|
-
}
|
|
38
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
|
|
3
|
-
export default function GameStatus ({ gameConnection }) {
|
|
4
|
-
const players = gameConnection.client.matchData
|
|
5
|
-
const winner = gameConnection.state.ctx.gameover?.winner
|
|
6
|
-
const draw = gameConnection.state.ctx.gameover?.draw
|
|
7
|
-
let winnerString = ''
|
|
8
|
-
if (draw) {
|
|
9
|
-
winnerString = 'Draw!'
|
|
10
|
-
} else if (players && winner) {
|
|
11
|
-
winnerString = `${players[winner].name} Wins!`
|
|
12
|
-
} else if (winner) {
|
|
13
|
-
winnerString = `Player ${winner} Wins!`
|
|
14
|
-
}
|
|
15
|
-
return gameConnection.state.ctx.gameover && (
|
|
16
|
-
<div className="game-status">
|
|
17
|
-
{winnerString}
|
|
18
|
-
</div>
|
|
19
|
-
)
|
|
20
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { useGame } from "../../contexts/game-context.js";
|
|
3
|
-
import Entity from '../entity/entity.js'
|
|
4
|
-
|
|
5
|
-
function calculateOptimalCols(numSquares) {
|
|
6
|
-
if (numSquares === 0) return 1;
|
|
7
|
-
|
|
8
|
-
let bestCols = 1;
|
|
9
|
-
let bestAspectRatio = Infinity;
|
|
10
|
-
|
|
11
|
-
for (let cols = 1; cols <= numSquares; cols++) {
|
|
12
|
-
const rows = Math.ceil(numSquares / cols);
|
|
13
|
-
const aspectRatio = Math.abs(cols - rows);
|
|
14
|
-
|
|
15
|
-
if (aspectRatio < bestAspectRatio) {
|
|
16
|
-
bestAspectRatio = aspectRatio;
|
|
17
|
-
bestCols = cols;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return bestCols;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export default function Space ({ space }) {
|
|
25
|
-
const { clickTarget, allClickable, currentMoveTargets } = useGame()
|
|
26
|
-
const { entities, entityId } = space.attributes
|
|
27
|
-
|
|
28
|
-
const clickable = allClickable.has(space)
|
|
29
|
-
const targeted = currentMoveTargets?.map(e => e.entityId).includes(entityId)
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<a
|
|
33
|
-
className={[
|
|
34
|
-
'space',
|
|
35
|
-
clickable && 'space--clickable',
|
|
36
|
-
targeted && 'space--targeted'
|
|
37
|
-
].filter(Boolean).join(' ')}
|
|
38
|
-
onClick={() => clickTarget(space)}
|
|
39
|
-
style={{
|
|
40
|
-
display: 'inline-block',
|
|
41
|
-
flex: '1',
|
|
42
|
-
}}
|
|
43
|
-
>
|
|
44
|
-
<div
|
|
45
|
-
className="space__entity-grid"
|
|
46
|
-
style={{
|
|
47
|
-
display: 'flex',
|
|
48
|
-
height: '100%',
|
|
49
|
-
width: '100%',
|
|
50
|
-
flexWrap: 'wrap',
|
|
51
|
-
}}
|
|
52
|
-
>
|
|
53
|
-
{Array.from({ length: entities.length }, (_, i) => (
|
|
54
|
-
<div
|
|
55
|
-
className="space__entity-grid__cell"
|
|
56
|
-
style={{
|
|
57
|
-
display: 'inline-block',
|
|
58
|
-
}}
|
|
59
|
-
key={i}
|
|
60
|
-
>
|
|
61
|
-
<Entity entity={entities[i]} />
|
|
62
|
-
</div>
|
|
63
|
-
))}
|
|
64
|
-
{!entities.length && space.attributes.name}
|
|
65
|
-
</div>
|
|
66
|
-
</a>
|
|
67
|
-
);
|
|
68
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import React, { createContext, useContext, useEffect } from 'react';
|
|
2
|
-
|
|
3
|
-
const GameContext = createContext({
|
|
4
|
-
clickTarget: () => {},
|
|
5
|
-
});
|
|
6
|
-
|
|
7
|
-
// do we need isSpectator
|
|
8
|
-
export function GameProvider({ gameConnection, children, isSpectator }) {
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
if (gameConnection.state._stateID === 0) {
|
|
11
|
-
gameConnection.reset()
|
|
12
|
-
}
|
|
13
|
-
}, [gameConnection.state._stateID]);
|
|
14
|
-
|
|
15
|
-
return (
|
|
16
|
-
<GameContext.Provider value={{
|
|
17
|
-
clickTarget: target => {
|
|
18
|
-
if (!isSpectator) {
|
|
19
|
-
gameConnection.doStep(target)
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
undoStep: () => { gameConnection.undoStep() },
|
|
23
|
-
allClickable: (gameConnection.optimisticWinner || isSpectator)
|
|
24
|
-
? new Set()
|
|
25
|
-
: gameConnection.allClickable,
|
|
26
|
-
currentMoveTargets: (gameConnection.optimisticWinner || isSpectator)
|
|
27
|
-
? []
|
|
28
|
-
: gameConnection.moveBuilder.targets,
|
|
29
|
-
}}>
|
|
30
|
-
{children}
|
|
31
|
-
</GameContext.Provider>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const useGame = () => useContext(GameContext);
|
package/src/index.js
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { useEffect, useReducer, useState } from 'react'
|
|
2
|
-
import { Client } from 'board-game-engine'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export const useGameserverConnection = ({
|
|
6
|
-
server,
|
|
7
|
-
multiplayer,
|
|
8
|
-
matchID,
|
|
9
|
-
gameRules,
|
|
10
|
-
gameName,
|
|
11
|
-
boardgameIOGame,
|
|
12
|
-
playerID,
|
|
13
|
-
credentials,
|
|
14
|
-
numPlayers,
|
|
15
|
-
debug,
|
|
16
|
-
enabled = true,
|
|
17
|
-
}) => {
|
|
18
|
-
const [_, forceUpdate] = useReducer(x => x + 1, 0)
|
|
19
|
-
const [connection, setConnection] = useState(null)
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (
|
|
23
|
-
!gameRules && !boardgameIOGame
|
|
24
|
-
|| credentials && !(matchID && enabled && server)
|
|
25
|
-
) {
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const options = {
|
|
30
|
-
server,
|
|
31
|
-
numPlayers,
|
|
32
|
-
onClientUpdate: () => {
|
|
33
|
-
forceUpdate()
|
|
34
|
-
},
|
|
35
|
-
debug,
|
|
36
|
-
matchID,
|
|
37
|
-
gameRules,
|
|
38
|
-
boardgameIOGame,
|
|
39
|
-
gameName,
|
|
40
|
-
playerID,
|
|
41
|
-
credentials,
|
|
42
|
-
multiplayer
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const newConnection = new Client(options)
|
|
46
|
-
|
|
47
|
-
newConnection.connect()
|
|
48
|
-
|
|
49
|
-
setConnection(newConnection)
|
|
50
|
-
|
|
51
|
-
return () => {
|
|
52
|
-
connection?.client?.stop()
|
|
53
|
-
setConnection(null)
|
|
54
|
-
}
|
|
55
|
-
}, [
|
|
56
|
-
matchID,
|
|
57
|
-
server,
|
|
58
|
-
playerID,
|
|
59
|
-
credentials,
|
|
60
|
-
gameRules,
|
|
61
|
-
boardgameIOGame,
|
|
62
|
-
enabled,
|
|
63
|
-
multiplayer,
|
|
64
|
-
])
|
|
65
|
-
|
|
66
|
-
if (connection) {
|
|
67
|
-
return Object.assign(
|
|
68
|
-
connection,
|
|
69
|
-
connection?.getState?.(),
|
|
70
|
-
)
|
|
71
|
-
} else {
|
|
72
|
-
return {}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { Game, useGameserverConnection } from '../src/index'
|
|
3
|
-
import ticTacToe from './tic-tac-toe.json'
|
|
4
|
-
import { ErrorBoundary } from 'react-error-boundary'
|
|
5
|
-
import './styles.css'
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
title: 'Game',
|
|
9
|
-
component: Game,
|
|
10
|
-
className: 'board-game-engine-react',
|
|
11
|
-
args: {
|
|
12
|
-
gameRules: JSON.stringify(ticTacToe, null, 2),
|
|
13
|
-
numPlayers: 2,
|
|
14
|
-
isSpectator: false,
|
|
15
|
-
},
|
|
16
|
-
argTypes: {
|
|
17
|
-
gameRules: { control: 'text' },
|
|
18
|
-
numPlayers: { control: 'number' },
|
|
19
|
-
isSpectator: { control: 'boolean' },
|
|
20
|
-
},
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const parseGameRules = (gameRules, fallback) => {
|
|
24
|
-
if (typeof gameRules !== 'string') return gameRules
|
|
25
|
-
try {
|
|
26
|
-
return JSON.parse(gameRules || '{}')
|
|
27
|
-
} catch {
|
|
28
|
-
return fallback
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const fallbackRender = ({ error }) => <div>Invalid Game Rules {error}</div>
|
|
33
|
-
|
|
34
|
-
const Template = ({ gameRules, numPlayers, loading, isSpectator }) => {
|
|
35
|
-
const rules = parseGameRules(gameRules, ticTacToe)
|
|
36
|
-
const gameConnection = useGameserverConnection({
|
|
37
|
-
gameRules: rules,
|
|
38
|
-
numPlayers,
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<div className="story">
|
|
43
|
-
<ErrorBoundary resetKeys={[gameRules, numPlayers]} fallbackRender={fallbackRender}>
|
|
44
|
-
<Game
|
|
45
|
-
gameConnection={gameConnection}
|
|
46
|
-
loading={loading}
|
|
47
|
-
isSpectator={isSpectator}
|
|
48
|
-
/>
|
|
49
|
-
</ErrorBoundary>
|
|
50
|
-
</div>
|
|
51
|
-
)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export const Basic = Template.bind({})
|
|
55
|
-
|
|
56
|
-
export const WithLoading = (args) => (
|
|
57
|
-
<Template
|
|
58
|
-
{...args}
|
|
59
|
-
loading={<div className="story-loading">Loading game…</div>}
|
|
60
|
-
/>
|
|
61
|
-
)
|
|
62
|
-
WithLoading.args = {
|
|
63
|
-
gameRules: JSON.stringify(ticTacToe, null, 2),
|
|
64
|
-
numPlayers: 2,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export const Spectator = Template.bind({})
|
|
68
|
-
Spectator.args = {
|
|
69
|
-
gameRules: JSON.stringify(ticTacToe, null, 2),
|
|
70
|
-
numPlayers: 2,
|
|
71
|
-
isSpectator: true,
|
|
72
|
-
}
|