board-game-engine-react 0.0.1

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/.eslintrc.js ADDED
@@ -0,0 +1,24 @@
1
+ module.exports = {
2
+ env: {
3
+ browser: true,
4
+ es6: true
5
+ },
6
+ extends: ['plugin:react/recommended', 'standard', 'plugin:storybook/recommended'],
7
+ globals: {
8
+ Atomics: 'readonly',
9
+ SharedArrayBuffer: 'readonly'
10
+ },
11
+ parserOptions: {
12
+ ecmaFeatures: {
13
+ jsx: true
14
+ },
15
+ ecmaVersion: 'latest',
16
+ sourceType: 'module'
17
+ },
18
+ plugins: [
19
+ 'react'
20
+ ],
21
+ rules: {
22
+ 'operator-linebreak': ['error', 'before']
23
+ }
24
+ }
@@ -0,0 +1 @@
1
+ import '@storybook/addon-controls/register';
@@ -0,0 +1,18 @@
1
+ /** @type { import('@storybook/react-webpack5').StorybookConfig } */
2
+ const config = {
3
+ stories: ["../stories/**/*.stories.@(js|jsx|ts|tsx)"],
4
+ addons: [
5
+ "@storybook/addon-webpack5-compiler-swc",
6
+ "@storybook/addon-onboarding",
7
+ "@storybook/addon-essentials",
8
+ "@chromatic-com/storybook",
9
+ "@storybook/addon-interactions",
10
+ "@storybook/preset-scss"
11
+ ],
12
+ framework: {
13
+ name: "@storybook/react-webpack5",
14
+ options: {},
15
+ },
16
+ staticDirs: ['../stories/static'],
17
+ };
18
+ export default config;
@@ -0,0 +1,24 @@
1
+ const path = require('path')
2
+
3
+ const modulesToTranspile = [
4
+ 'react-dialogue-tree'
5
+ ]
6
+
7
+ module.exports = ({ config, mode }) => {
8
+ config.resolve.alias['react-yarn-visual-novel'] = mode === 'PRODUCTION'
9
+ ? path.resolve(__dirname, '../dist/react-yarn-visual-novel.min.js')
10
+ : path.resolve(__dirname, '../src/index.js')
11
+
12
+ // brittle
13
+ const babelLoader = config.module.rules[config.module.rules.length - 1]
14
+ babelLoader.exclude.shift()
15
+ babelLoader.exclude.unshift(new RegExp(
16
+ `node_modules\/(?!${modulesToTranspile.join('|')}).+`)
17
+ )
18
+
19
+ config.resolve.fallback = {
20
+ fs: false,
21
+ path: false,
22
+ }
23
+ return config
24
+ }
package/README.md ADDED
@@ -0,0 +1,4 @@
1
+ # Board Game Engine React
2
+
3
+ React-based runner for games using board-game-engine
4
+
@@ -0,0 +1,9 @@
1
+ module.exports = {
2
+ presets: [
3
+ '@babel/preset-react',
4
+ ['@babel/preset-env', { targets: { node: 'current' } }]
5
+ ],
6
+ plugins: [
7
+ '@babel/plugin-proposal-object-rest-spread'
8
+ ]
9
+ }
@@ -0,0 +1,12 @@
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
+ })
@@ -0,0 +1,2 @@
1
+ html, body, #storybook-root, .story {
2
+ height: 100%; }
@@ -0,0 +1,20 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('react')) :
3
+ typeof define === 'function' && define.amd ? define(['react'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BoardGameEngineReact = factory(global.React));
5
+ })(this, (function (React) { 'use strict';
6
+
7
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
8
+
9
+ var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
10
+
11
+ function BoardGameEngineReact({}) {
12
+ return /*#__PURE__*/React__default["default"].createElement("div", {
13
+ className: "board-game-engine-react"
14
+ });
15
+ }
16
+ BoardGameEngineReact.propTypes = {};
17
+
18
+ return BoardGameEngineReact;
19
+
20
+ }));
@@ -0,0 +1 @@
1
+ html,body,#storybook-root,.story{height:100%}
@@ -0,0 +1 @@
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("react")):"function"==typeof define&&define.amd?define(["react"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).BoardGameEngineReact=t(e.React)}(this,function(e){"use strict";function t(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var n=t(e);function o({}){return n.default.createElement("div",{className:"board-game-engine-react"})}return o.propTypes={},o});
package/jest.config.js ADDED
@@ -0,0 +1,13 @@
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
+ }
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "board-game-engine-react",
3
+ "version": "0.0.1",
4
+ "description": "React library for using board-game-engine",
5
+ "main": "dist/board-game-engine-react.js",
6
+ "scripts": {
7
+ "start": "storybook dev p 6006",
8
+ "test": "jest --verbose false",
9
+ "test:coverage": "jest --coverage --verbose false",
10
+ "build": "rollup -c",
11
+ "storybook": "storybook dev -p 6006",
12
+ "prepare": "npm run build",
13
+ "build-storybook": "storybook build"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/mnbroatch/board-game-engine-react.git"
18
+ },
19
+ "keywords": [
20
+ "react",
21
+ "board game",
22
+ "boardgame",
23
+ "ui"
24
+ ],
25
+ "author": "Matthew Broatch",
26
+ "license": "ISC",
27
+ "bugs": {
28
+ "url": "https://github.com/mnbroatch/board-game-engine-react/issues"
29
+ },
30
+ "homepage": "https://github.com/mnbroatch/board-game-engine-react",
31
+ "devDependencies": {
32
+ "@babel/core": "^7.20.12",
33
+ "@babel/plugin-proposal-object-rest-spread": "",
34
+ "@babel/preset-env": "^7.26.0",
35
+ "@babel/preset-react": "",
36
+ "@chromatic-com/storybook": "^3.2.2",
37
+ "@rollup/plugin-babel": "",
38
+ "@rollup/plugin-commonjs": "",
39
+ "@rollup/plugin-json": "",
40
+ "@rollup/plugin-node-resolve": "^13.0.0",
41
+ "@storybook/addon-essentials": "^8.4.6",
42
+ "@storybook/addon-interactions": "^8.4.6",
43
+ "@storybook/addon-onboarding": "^8.4.6",
44
+ "@storybook/addon-webpack5-compiler-swc": "^1.0.5",
45
+ "@storybook/blocks": "^8.4.6",
46
+ "@storybook/preset-scss": "^1.0.3",
47
+ "@storybook/react": "^8.4.6",
48
+ "@storybook/react-webpack5": "^8.4.6",
49
+ "@storybook/test": "^8.4.6",
50
+ "@testing-library/jest-dom": "^5.16.1",
51
+ "@testing-library/react": "^13.4.0",
52
+ "@testing-library/user-event": "^13.5.0",
53
+ "assert": "^2.0.0",
54
+ "babel-jest": "^27.4.4",
55
+ "babel-loader": "^8.3.0",
56
+ "eslint": "",
57
+ "eslint-config-standard": "",
58
+ "eslint-plugin-import": "",
59
+ "eslint-plugin-node": "",
60
+ "eslint-plugin-promise": "",
61
+ "eslint-plugin-react": "",
62
+ "eslint-plugin-storybook": "^0.11.1",
63
+ "install": "^0.13.0",
64
+ "jest": "^27.4.3",
65
+ "markdown-to-jsx": "",
66
+ "node-sass": "^7.0.1",
67
+ "npm": "^8.3.1",
68
+ "prop-types": "^15.8.1",
69
+ "raw-loader": "",
70
+ "react-dom": "^18.2.0",
71
+ "react-syntax-highlighter": "",
72
+ "react-useinterval": "",
73
+ "rollup": "^2.63.0",
74
+ "rollup-plugin-node-resolve": "^5.2.0",
75
+ "rollup-plugin-scss": "^3.0.0",
76
+ "rollup-plugin-terser": "",
77
+ "sass-loader": "^12.4.0",
78
+ "source-loader": "",
79
+ "storybook": "^8.4.6"
80
+ },
81
+ "dependencies": {
82
+ "react": "^18.2.0",
83
+ "react-error-boundary": "^3.1.4",
84
+ "react-transition-group": "^4.4.5"
85
+ }
86
+ }
@@ -0,0 +1,42 @@
1
+ import babel from '@rollup/plugin-babel'
2
+ import { terser } from 'rollup-plugin-terser'
3
+ import scss from 'rollup-plugin-scss'
4
+ import resolve from 'rollup-plugin-node-resolve'
5
+
6
+ const config = [
7
+ {
8
+ input: 'src/index.js',
9
+ external: ['react'],
10
+ output: {
11
+ format: 'umd',
12
+ file: 'dist/board-game-engine-react.js',
13
+ name: 'BoardGameEngineReact'
14
+ },
15
+ plugins: [
16
+ resolve({
17
+ }),
18
+ babel({
19
+ }),
20
+ scss()
21
+ ]
22
+ },
23
+ {
24
+ input: 'src/index.js',
25
+ external: ['react'],
26
+ output: {
27
+ format: 'umd',
28
+ file: 'dist/board-game-engine-react.min.js',
29
+ name: 'BoardGameEngineReact'
30
+ },
31
+ plugins: [
32
+ resolve({
33
+ }),
34
+ babel({
35
+ }),
36
+ scss({ outputStyle: 'compressed' }),
37
+ terser()
38
+ ]
39
+ }
40
+ ]
41
+
42
+ export default config
@@ -0,0 +1,12 @@
1
+ import React, { useRef } from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ export default function BoardGameEngineReact ({}) {
5
+ return (
6
+ <div className="board-game-engine-react">
7
+ </div>
8
+ )
9
+ }
10
+
11
+ BoardGameEngineReact.propTypes = {
12
+ }
@@ -0,0 +1,45 @@
1
+ import React from 'react'
2
+ import { useGame } from "../contexts/game-context.js";
3
+
4
+ export default function AbstractChoices () {
5
+ const { clickTarget, allClickable, undoStep, currentMoveTargets } = useGame()
6
+
7
+ const abstractChoices = [...allClickable].filter(c => c.abstract)
8
+
9
+ // spacer assumes only one row of choices.
10
+ // could save and store biggest height instead?
11
+ return (
12
+ <div style={{ position: 'relative' }}>
13
+ <button
14
+ style={{visibility: 'hidden'}}
15
+ className="button button--style-b button--x-small abstract-choices__choice"
16
+ >
17
+ Spacer
18
+ </button>
19
+ <div
20
+ style={{
21
+ position: 'absolute',
22
+ top: 0,
23
+ width: '100%',
24
+ }}>
25
+ {!!currentMoveTargets.length && (
26
+ <button
27
+ className="button button--style-c button--x-small abstract-choices__choice abstract-choices__choice--undo"
28
+ onClick={undoStep}
29
+ >
30
+ Undo
31
+ </button>
32
+ )}
33
+ {abstractChoices.map((choice, i) => (
34
+ <button
35
+ key={i}
36
+ className="button button--style-b button--x-small abstract-choices__choice"
37
+ onClick={() => clickTarget(choice)}
38
+ >
39
+ {choice.value}
40
+ </button>
41
+ ))}
42
+ </div>
43
+ </div>
44
+ )
45
+ }
@@ -0,0 +1,28 @@
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
+ }
@@ -0,0 +1,35 @@
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);
@@ -0,0 +1,37 @@
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
+
11
+ switch (attributes.type) {
12
+ case 'Grid':
13
+ return <Grid grid={entity} isClickable={isClickable} />
14
+ case 'Space':
15
+ return <Space space={entity} isClickable={isClickable} />
16
+ default:
17
+ return <div
18
+ onClick={(e) => {
19
+ if (isClickable) {
20
+ e.stopPropagation()
21
+ clickTarget(entity)
22
+ }
23
+ }}
24
+ className={[
25
+ 'entity',
26
+ attributes.player && `player-${attributes.player}`,
27
+ allClickable.has(entity) && 'entity--clickable',
28
+ ].filter(Boolean).join(' ')}
29
+ >
30
+ {entity.rule.displayProperties?.map((property, i) => (
31
+ <div key={i}>
32
+ {property}: {(entity.attributes[property])?.toString()}
33
+ </div>
34
+ ))}
35
+ </div>
36
+ }
37
+ }
@@ -0,0 +1,56 @@
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";
6
+
7
+ export default function Game ({ gameConnection }) {
8
+ console.log('555gameConnection', gameConnection)
9
+ const { G } = gameConnection.state
10
+
11
+ return (
12
+ <GameProvider
13
+ gameConnection={gameConnection}
14
+ isSpectator
15
+ >
16
+ <div className="game">
17
+ <AbstractChoices />
18
+ <div
19
+ className="shared-board"
20
+ style={{
21
+ width: '100%',
22
+ display: 'flex',
23
+ flexWrap: 'wrap',
24
+ justifyContent: 'center',
25
+ alignItems: 'center',
26
+ gap: '1em',
27
+ }}
28
+ >
29
+ {G.sharedBoard.entities.map((entity, i) => <Entity key={i} entity={entity} />)}
30
+ </div>
31
+ {G.personalBoards && (
32
+ <div className="personal-boards">
33
+ {G.personalBoards.map((board, i) => (
34
+ <div
35
+ key={i}
36
+ className="personal-board"
37
+ style={{
38
+ width: '100%',
39
+ display: 'grid',
40
+ gridAutoFlow: 'column',
41
+ gridAutoRows: '1fr',
42
+ gap: '1em',
43
+ }}
44
+ >
45
+ {board.entities.map((entity, j) => (
46
+ <Entity key={j} entity={entity} />
47
+ ))}
48
+ </div>
49
+ ))}
50
+ </div>
51
+ )}
52
+ <GameStatus gameConnection={gameConnection} />
53
+ </div>
54
+ </GameProvider>
55
+ )
56
+ }
@@ -0,0 +1,20 @@
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
+ }
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import './styles.scss'
2
+
3
+ export { default } from './BoardGameEngineReact.js'
@@ -0,0 +1,68 @@
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].map(e => e.entityId).includes(entityId)
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
+ }
@@ -0,0 +1,4 @@
1
+ html, body, #storybook-root, .story, {
2
+ height: 100%;
3
+ }
4
+
@@ -0,0 +1,52 @@
1
+ import React from 'react'
2
+ import BoardGameEngineReact from '../src/index'
3
+ import { ErrorBoundary } from 'react-error-boundary'
4
+ import './styles.css'
5
+
6
+ const dialogue = `
7
+ title: Start
8
+ ---
9
+ Red: Hello.
10
+ Blue: Hi!
11
+ Red: I'm here.
12
+ Blue: I'm here too!
13
+ ===
14
+ `
15
+
16
+ export default {
17
+ title: 'BoardGameEngineReact',
18
+ component: BoardGameEngineReact,
19
+ className: 'board-game-engine-react',
20
+ args: {
21
+ dialogue,
22
+ },
23
+ argTypes: {
24
+ dialogue: {
25
+ control: 'text'
26
+ }
27
+ }
28
+ }
29
+
30
+ const Template = (props) => {
31
+ return (
32
+ <div className="story">
33
+ <ErrorBoundary
34
+ resetKeys={[props.dialogue]}
35
+ fallbackRender={({ error }) => {
36
+ return (
37
+ <div>
38
+ Invalid Dialogue: {error.message}
39
+ </div>
40
+ )
41
+ }}
42
+ >
43
+ <BoardGameEngineReact
44
+ {...props}
45
+ onDialogueEnd={() => { alert('onDialogueEnd called') }}
46
+ />
47
+ </ErrorBoundary>
48
+ </div>
49
+ )
50
+ }
51
+
52
+ export const Basic = Template.bind({})
Binary file
Binary file
File without changes