board-game-engine 0.0.11 → 1.0.3
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/README.md +138 -1
- package/dist/board-game-engine.cjs +35 -36
- package/dist/board-game-engine.js +35 -36
- package/dist/board-game-engine.min.js +20 -20
- package/dist/board-game-engine.mjs +35 -36
- package/e2e/coverage.js +33 -0
- package/package.json +2 -1
- package/playwright-report/index.html +1 -1
- package/src/client/client.js +41 -38
package/README.md
CHANGED
|
@@ -1 +1,138 @@
|
|
|
1
|
-
|
|
1
|
+
# board-game-engine
|
|
2
|
+
|
|
3
|
+
Runs games written with the early-in-development JSON-based B.A.G.E.L. (Board-based Automated Game Engine Language). Built upon [boardgame.io](https://boardgame.io/)
|
|
4
|
+
|
|
5
|
+
Currently supports enough rules flexibility to describe:
|
|
6
|
+
- Tic Tac Toe
|
|
7
|
+
- Checkers
|
|
8
|
+
- Othello
|
|
9
|
+
- 4-in-a-Row But There is Gravity
|
|
10
|
+
- Crazy Eights
|
|
11
|
+
|
|
12
|
+
[**B.A.G.E.L. docs:**](https://boardgameengine.com/docs/index.html) — reference for moves, conditions, values, shorthand, and examples.
|
|
13
|
+
[**board-game-engine-react**](https://github.com/mnbroatch/board-game-engine-react) — react component wrapping this repo
|
|
14
|
+
[**boardgameengine.com**](https://boardgameengine.com) — Main page
|
|
15
|
+
- Create custom games or edit
|
|
16
|
+
- Create multiplayer lobbies for playing custom B.A.G.E.L. games, also includes client-side editor sandbox. Maintained by author of board-game-engine
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Benefits
|
|
21
|
+
|
|
22
|
+
The B.A.G.E.L. Domain-Specific Langage:
|
|
23
|
+
- declarative
|
|
24
|
+
- safe because there is no custom code to run
|
|
25
|
+
- this means it would be hard for, say, bad LLM output to embed anything too nasty inside "user"-defined games
|
|
26
|
+
- complete enough to produce multiplayer web prototypes
|
|
27
|
+
|
|
28
|
+
Using B.A.G.E.L. in conjunction with this engine also enables UX features:
|
|
29
|
+
- client-side staging for multi-step moves
|
|
30
|
+
- You can for instance select a piece, then select a destination to put it at, all before committing the move
|
|
31
|
+
- Undo steps before commit
|
|
32
|
+
- Highlight currently-playable targets during a move
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install board-game-engine
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Public API
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
### `Client`
|
|
48
|
+
|
|
49
|
+
A client that runs a B.A.G.E.L.-defined game
|
|
50
|
+
|
|
51
|
+
**Constructor: `new Client(options)`**
|
|
52
|
+
|
|
53
|
+
| Option | Type | Description |
|
|
54
|
+
|--------|------|-------------|
|
|
55
|
+
| `gameRules` | string | JSON string of the B.A.G.E.L. game definition |
|
|
56
|
+
| `numPlayers` | number | Number of players in client-side game. |
|
|
57
|
+
| `onClientUpdate` | function | Callback after state updates (e.g. to re-render UI). |
|
|
58
|
+
| `debug` | object | boardgame.io debug panel config; e.g. `false`. |
|
|
59
|
+
|
|
60
|
+
**Multiplayer** — For connecting to a remote game, see [Multiplayer](#multiplayer) below. Options such as `server` and `matchId` are passed through to the boardgame.io client; see the [boardgame.io Client API](https://boardgame.io/documentation/#/api/Client) for details.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
**Methods**
|
|
64
|
+
|
|
65
|
+
- **`connect()`** — Connects to the game (local or server), starts the client, subscribes to updates. Returns `this`.
|
|
66
|
+
- **`getState()`** — Returns current state. See [getState() return value](#getstate-return-value) below.
|
|
67
|
+
- **`update()`** — Triggers `onClientUpdate` if set.
|
|
68
|
+
- **`doStep(target)`** — Applies one step of a multi-step move (e.g. “from” then “to”). For B.A.G.E.L. games only.
|
|
69
|
+
- **`undoStep()`** — Undoes the last step of the current move. B.A.G.E.L. games only.
|
|
70
|
+
- **`reset()`** — Clears the current move builder (targets/steps). B.A.G.E.L. games only.
|
|
71
|
+
|
|
72
|
+
**Properties**
|
|
73
|
+
|
|
74
|
+
- **`client`** — boardgame.io client instance
|
|
75
|
+
- **`moveBuilder`** — Mostly for internal use with multi-step moves but could inform UI
|
|
76
|
+
|
|
77
|
+
#### getState() return value
|
|
78
|
+
|
|
79
|
+
`Client` can run normal boardgame.io games, with limited features compared to B.A.G.E.L. games.
|
|
80
|
+
|
|
81
|
+
In non-B.A.G.E.L. games, `doStep(target)` is not used. instead, the boardgame.io client's moves object is returned from getState().
|
|
82
|
+
|
|
83
|
+
- `state` — current game state
|
|
84
|
+
- `gameover` — game-over result when the game has ended
|
|
85
|
+
- `allClickable` — B.A.G.E.L. games only. Clickable targets for the current step of the current move
|
|
86
|
+
- `moves` — non-B.A.G.E.L. games only. From boardgame.io client
|
|
87
|
+
- `currentMoves` — non-B.A.G.E.L. games only. `moves` object filtered to only contain current phase / stage moves. May break for complex turns.
|
|
88
|
+
|
|
89
|
+
**Example**
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
import { Client } from 'board-game-engine'
|
|
93
|
+
|
|
94
|
+
const client = new Client({
|
|
95
|
+
gameRules: JSON.stringify(myGameRules),
|
|
96
|
+
numPlayers: 2,
|
|
97
|
+
onClientUpdate: () => render(client.getState())
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
client.connect()
|
|
101
|
+
// Later, when user picks a target (e.g. a cell or piece):
|
|
102
|
+
client.doStep(target)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### Multiplayer
|
|
106
|
+
|
|
107
|
+
To connect to a remote match instead of running client-only, pass `server`, `matchId`, `playerID`, and `credentials`. When `credentials` is absent, the client runs in client-only mode.
|
|
108
|
+
|
|
109
|
+
Note: boardgame.io does not allow adding games to the server instance after it is instantiated. boardgameengine.com hacks around that, but your server will need to have access to any games you intend to play when you boot it up.
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
| Option | Type | Description |
|
|
113
|
+
|--------|------|-------------|
|
|
114
|
+
| `server` | string | URL of gameserver running boardgame.io server instance |
|
|
115
|
+
| `matchId` | string | Match ID. |
|
|
116
|
+
| `gameName` | string | Game name |
|
|
117
|
+
| `playerID` | string | Player ID (first player is `'0'`, next is `'1'`, etc.) |
|
|
118
|
+
| `credentials` | string | Credentials for your server to interpret |
|
|
119
|
+
| `multiplayer` | object | boardgame.io multiplayer transport. Defaults to `SocketIO({ server, socketOpts: { transports: ['websocket', 'polling'] } })`, (SocketIO is exported from `boardgame.io/multiplayer` |
|
|
120
|
+
|
|
121
|
+
All options are required, (only one of `server` or `multiplayer`). For full details on the client options and multiplayer setup, see the [boardgame.io Client API](https://boardgame.io/documentation/#/api/Client).
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### `gameFactory(gameRules, gameName)`
|
|
126
|
+
|
|
127
|
+
Builds a boardgame.io-compatible game from a B.A.G.E.L. game definition. Useful for preloading games in server code (server games must be preloaded in boardgame.io)
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## How it fits
|
|
132
|
+
|
|
133
|
+
1. You write a B.A.G.E.L. game definition (JSON).
|
|
134
|
+
2. **board-game-engine** turns it into a boardgame.io game
|
|
135
|
+
3. The game runs with boardgame.io (local or server).
|
|
136
|
+
4. `Client` wrapper adds client-side functionality like valid move highlighting
|
|
137
|
+
|
|
138
|
+
For the full language reference, examples, and getting started, see **[https://boardgameengine.com/docs/index.html](https://boardgameengine.com/docs/index.html)**.
|
|
@@ -27071,7 +27071,7 @@ function createPayload(bgioState, moveRule, targets, context) {
|
|
|
27071
27071
|
var Client2 = class {
|
|
27072
27072
|
constructor(options) {
|
|
27073
27073
|
this.options = options;
|
|
27074
|
-
this.game = options.boardgameIOGame || gameFactory(JSON.parse(options.gameRules)
|
|
27074
|
+
this.game = options.boardgameIOGame || gameFactory(JSON.parse(options.gameRules));
|
|
27075
27075
|
if (!options.boardgameIOGame) {
|
|
27076
27076
|
this.moveBuilder = { targets: [], stepIndex: 0, eliminatedMoves: [] };
|
|
27077
27077
|
this.optimisticWinner = null;
|
|
@@ -27085,18 +27085,18 @@ var Client2 = class {
|
|
|
27085
27085
|
collapseOnLoad: true,
|
|
27086
27086
|
impl: Debug
|
|
27087
27087
|
},
|
|
27088
|
-
|
|
27089
|
-
|
|
27090
|
-
|
|
27091
|
-
|
|
27088
|
+
matchID,
|
|
27089
|
+
playerID,
|
|
27090
|
+
credentials,
|
|
27091
|
+
multiplayer = SocketIO({ server, socketOpts: { transports: ["websocket", "polling"] } })
|
|
27092
27092
|
} = this.options;
|
|
27093
27093
|
try {
|
|
27094
|
-
const clientOptions =
|
|
27094
|
+
const clientOptions = !credentials ? { game: this.game, numPlayers, debug } : {
|
|
27095
27095
|
game: this.game,
|
|
27096
|
-
multiplayer
|
|
27097
|
-
matchID
|
|
27098
|
-
playerID
|
|
27099
|
-
credentials
|
|
27096
|
+
multiplayer,
|
|
27097
|
+
matchID,
|
|
27098
|
+
playerID,
|
|
27099
|
+
credentials,
|
|
27100
27100
|
debug
|
|
27101
27101
|
};
|
|
27102
27102
|
this.client = Client(clientOptions);
|
|
@@ -27112,46 +27112,45 @@ var Client2 = class {
|
|
|
27112
27112
|
this.options.onClientUpdate?.();
|
|
27113
27113
|
}
|
|
27114
27114
|
getState() {
|
|
27115
|
-
const
|
|
27116
|
-
if (!
|
|
27115
|
+
const bgioState = this.client?.getState();
|
|
27116
|
+
if (!bgioState) return {};
|
|
27117
|
+
const state = this.options.boardgameIOGame ? bgioState : {
|
|
27118
|
+
...bgioState,
|
|
27119
|
+
G: deserialize(JSON.stringify(bgioState.G), registry)
|
|
27120
|
+
};
|
|
27121
|
+
const gameover = this.optimisticWinner ?? state?.ctx?.gameover;
|
|
27122
|
+
const currentMoves = gameover ? [] : getCurrentMoves(state, this.client);
|
|
27117
27123
|
if (this.options.boardgameIOGame) {
|
|
27118
27124
|
return {
|
|
27119
|
-
state
|
|
27120
|
-
gameover
|
|
27121
|
-
moves: this.client.moves
|
|
27125
|
+
state,
|
|
27126
|
+
gameover,
|
|
27127
|
+
moves: this.client.moves,
|
|
27128
|
+
currentMoves
|
|
27122
27129
|
};
|
|
27123
27130
|
}
|
|
27124
|
-
const
|
|
27125
|
-
...clientState,
|
|
27126
|
-
G: deserialize(JSON.stringify(clientState.G), registry),
|
|
27127
|
-
originalG: clientState.G
|
|
27128
|
-
};
|
|
27129
|
-
const gameover = state?.ctx?.gameover;
|
|
27130
|
-
const moves = !gameover ? Object.entries(getCurrentMoves(state, this.client)).reduce((acc, [moveName, rawMove]) => {
|
|
27131
|
+
const _wrappedMoves = Object.entries(currentMoves).reduce((acc, [moveName, rawMove]) => {
|
|
27131
27132
|
const move = (payload) => {
|
|
27132
27133
|
this.client.moves[moveName](preparePayload(payload));
|
|
27133
27134
|
};
|
|
27134
27135
|
move.moveInstance = rawMove.moveInstance;
|
|
27135
27136
|
return { ...acc, [moveName]: move };
|
|
27136
|
-
}, {})
|
|
27137
|
-
const
|
|
27138
|
-
|
|
27139
|
-
const possibleMoveMeta = possibleMoves.possibleMoveMeta;
|
|
27140
|
-
return { state, gameover, moves, allClickable, possibleMoveMeta };
|
|
27137
|
+
}, {});
|
|
27138
|
+
const { allClickable, _possibleMoveMeta } = getPossibleMoves(state, _wrappedMoves, this.moveBuilder);
|
|
27139
|
+
return { state, gameover, allClickable, _wrappedMoves, _possibleMoveMeta };
|
|
27141
27140
|
}
|
|
27142
27141
|
doStep(_target) {
|
|
27143
27142
|
if (this.options.boardgameIOGame) return;
|
|
27144
|
-
const { state,
|
|
27143
|
+
const { state, _wrappedMoves, _possibleMoveMeta } = this.getState();
|
|
27145
27144
|
const target = _target.abstract ? _target : state.G.bank.locate(_target.entityId);
|
|
27146
|
-
const newEliminated = Object.entries(
|
|
27147
|
-
if (newEliminated.length === Object.keys(
|
|
27145
|
+
const newEliminated = Object.entries(_possibleMoveMeta).filter(([_2, meta]) => !hasTarget(meta.clickableForMove, target)).map(([name]) => name).concat(this.moveBuilder.eliminatedMoves);
|
|
27146
|
+
if (newEliminated.length === Object.keys(_wrappedMoves).length) {
|
|
27148
27147
|
console.error("invalid move with target:", target?.rule);
|
|
27149
27148
|
return;
|
|
27150
27149
|
}
|
|
27151
|
-
const remainingMoveEntries = Object.entries(
|
|
27152
|
-
if (isMoveCompleted(state,
|
|
27150
|
+
const remainingMoveEntries = Object.entries(_possibleMoveMeta).filter(([name]) => !newEliminated.includes(name));
|
|
27151
|
+
if (isMoveCompleted(state, _wrappedMoves, remainingMoveEntries, this.moveBuilder.stepIndex)) {
|
|
27153
27152
|
const [moveName] = remainingMoveEntries[0];
|
|
27154
|
-
const move =
|
|
27153
|
+
const move = _wrappedMoves[moveName];
|
|
27155
27154
|
const payload = createPayload(
|
|
27156
27155
|
state,
|
|
27157
27156
|
move.moveInstance.rule,
|
|
@@ -27194,7 +27193,7 @@ function hasTarget(clickableSet, target) {
|
|
|
27194
27193
|
}
|
|
27195
27194
|
function getPossibleMoves(bgioState, moves, moveBuilder) {
|
|
27196
27195
|
const { eliminatedMoves, stepIndex } = moveBuilder;
|
|
27197
|
-
const
|
|
27196
|
+
const _possibleMoveMeta = {};
|
|
27198
27197
|
const allClickable = /* @__PURE__ */ new Set();
|
|
27199
27198
|
Object.entries(moves).filter(([moveName]) => !eliminatedMoves.includes(moveName)).forEach(([moveName, move]) => {
|
|
27200
27199
|
const moveRule = resolveProperties(bgioState, { ...move.moveInstance.rule, moveName });
|
|
@@ -27212,10 +27211,10 @@ function getPossibleMoves(bgioState, moves, moveBuilder) {
|
|
|
27212
27211
|
const clickableForMove = new Set(
|
|
27213
27212
|
moveIsAllowed && moveSteps?.[stepIndex]?.getClickable(context) || []
|
|
27214
27213
|
);
|
|
27215
|
-
|
|
27214
|
+
_possibleMoveMeta[moveName] = { clickableForMove };
|
|
27216
27215
|
clickableForMove.forEach((entity) => allClickable.add(entity));
|
|
27217
27216
|
});
|
|
27218
|
-
return {
|
|
27217
|
+
return { _possibleMoveMeta, allClickable };
|
|
27219
27218
|
}
|
|
27220
27219
|
function isMoveCompleted(state, moves, remainingMoveEntries, stepIndex) {
|
|
27221
27220
|
return remainingMoveEntries.length === 1 && getSteps(state, moves[remainingMoveEntries[0][0]].moveInstance.rule).length === stepIndex + 1;
|
|
@@ -27071,7 +27071,7 @@ ${message}`);
|
|
|
27071
27071
|
var Client2 = class {
|
|
27072
27072
|
constructor(options) {
|
|
27073
27073
|
this.options = options;
|
|
27074
|
-
this.game = options.boardgameIOGame || gameFactory(JSON.parse(options.gameRules)
|
|
27074
|
+
this.game = options.boardgameIOGame || gameFactory(JSON.parse(options.gameRules));
|
|
27075
27075
|
if (!options.boardgameIOGame) {
|
|
27076
27076
|
this.moveBuilder = { targets: [], stepIndex: 0, eliminatedMoves: [] };
|
|
27077
27077
|
this.optimisticWinner = null;
|
|
@@ -27085,18 +27085,18 @@ ${message}`);
|
|
|
27085
27085
|
collapseOnLoad: true,
|
|
27086
27086
|
impl: Debug
|
|
27087
27087
|
},
|
|
27088
|
-
|
|
27089
|
-
|
|
27090
|
-
|
|
27091
|
-
|
|
27088
|
+
matchID,
|
|
27089
|
+
playerID,
|
|
27090
|
+
credentials,
|
|
27091
|
+
multiplayer = SocketIO({ server, socketOpts: { transports: ["websocket", "polling"] } })
|
|
27092
27092
|
} = this.options;
|
|
27093
27093
|
try {
|
|
27094
|
-
const clientOptions =
|
|
27094
|
+
const clientOptions = !credentials ? { game: this.game, numPlayers, debug } : {
|
|
27095
27095
|
game: this.game,
|
|
27096
|
-
multiplayer
|
|
27097
|
-
matchID
|
|
27098
|
-
playerID
|
|
27099
|
-
credentials
|
|
27096
|
+
multiplayer,
|
|
27097
|
+
matchID,
|
|
27098
|
+
playerID,
|
|
27099
|
+
credentials,
|
|
27100
27100
|
debug
|
|
27101
27101
|
};
|
|
27102
27102
|
this.client = Client(clientOptions);
|
|
@@ -27112,46 +27112,45 @@ ${message}`);
|
|
|
27112
27112
|
this.options.onClientUpdate?.();
|
|
27113
27113
|
}
|
|
27114
27114
|
getState() {
|
|
27115
|
-
const
|
|
27116
|
-
if (!
|
|
27115
|
+
const bgioState = this.client?.getState();
|
|
27116
|
+
if (!bgioState) return {};
|
|
27117
|
+
const state = this.options.boardgameIOGame ? bgioState : {
|
|
27118
|
+
...bgioState,
|
|
27119
|
+
G: deserialize(JSON.stringify(bgioState.G), registry)
|
|
27120
|
+
};
|
|
27121
|
+
const gameover = this.optimisticWinner ?? state?.ctx?.gameover;
|
|
27122
|
+
const currentMoves = gameover ? [] : getCurrentMoves(state, this.client);
|
|
27117
27123
|
if (this.options.boardgameIOGame) {
|
|
27118
27124
|
return {
|
|
27119
|
-
state
|
|
27120
|
-
gameover
|
|
27121
|
-
moves: this.client.moves
|
|
27125
|
+
state,
|
|
27126
|
+
gameover,
|
|
27127
|
+
moves: this.client.moves,
|
|
27128
|
+
currentMoves
|
|
27122
27129
|
};
|
|
27123
27130
|
}
|
|
27124
|
-
const
|
|
27125
|
-
...clientState,
|
|
27126
|
-
G: deserialize(JSON.stringify(clientState.G), registry),
|
|
27127
|
-
originalG: clientState.G
|
|
27128
|
-
};
|
|
27129
|
-
const gameover = state?.ctx?.gameover;
|
|
27130
|
-
const moves = !gameover ? Object.entries(getCurrentMoves(state, this.client)).reduce((acc, [moveName, rawMove]) => {
|
|
27131
|
+
const _wrappedMoves = Object.entries(currentMoves).reduce((acc, [moveName, rawMove]) => {
|
|
27131
27132
|
const move = (payload) => {
|
|
27132
27133
|
this.client.moves[moveName](preparePayload(payload));
|
|
27133
27134
|
};
|
|
27134
27135
|
move.moveInstance = rawMove.moveInstance;
|
|
27135
27136
|
return { ...acc, [moveName]: move };
|
|
27136
|
-
}, {})
|
|
27137
|
-
const
|
|
27138
|
-
|
|
27139
|
-
const possibleMoveMeta = possibleMoves.possibleMoveMeta;
|
|
27140
|
-
return { state, gameover, moves, allClickable, possibleMoveMeta };
|
|
27137
|
+
}, {});
|
|
27138
|
+
const { allClickable, _possibleMoveMeta } = getPossibleMoves(state, _wrappedMoves, this.moveBuilder);
|
|
27139
|
+
return { state, gameover, allClickable, _wrappedMoves, _possibleMoveMeta };
|
|
27141
27140
|
}
|
|
27142
27141
|
doStep(_target) {
|
|
27143
27142
|
if (this.options.boardgameIOGame) return;
|
|
27144
|
-
const { state,
|
|
27143
|
+
const { state, _wrappedMoves, _possibleMoveMeta } = this.getState();
|
|
27145
27144
|
const target = _target.abstract ? _target : state.G.bank.locate(_target.entityId);
|
|
27146
|
-
const newEliminated = Object.entries(
|
|
27147
|
-
if (newEliminated.length === Object.keys(
|
|
27145
|
+
const newEliminated = Object.entries(_possibleMoveMeta).filter(([_2, meta]) => !hasTarget(meta.clickableForMove, target)).map(([name]) => name).concat(this.moveBuilder.eliminatedMoves);
|
|
27146
|
+
if (newEliminated.length === Object.keys(_wrappedMoves).length) {
|
|
27148
27147
|
console.error("invalid move with target:", target?.rule);
|
|
27149
27148
|
return;
|
|
27150
27149
|
}
|
|
27151
|
-
const remainingMoveEntries = Object.entries(
|
|
27152
|
-
if (isMoveCompleted(state,
|
|
27150
|
+
const remainingMoveEntries = Object.entries(_possibleMoveMeta).filter(([name]) => !newEliminated.includes(name));
|
|
27151
|
+
if (isMoveCompleted(state, _wrappedMoves, remainingMoveEntries, this.moveBuilder.stepIndex)) {
|
|
27153
27152
|
const [moveName] = remainingMoveEntries[0];
|
|
27154
|
-
const move =
|
|
27153
|
+
const move = _wrappedMoves[moveName];
|
|
27155
27154
|
const payload = createPayload(
|
|
27156
27155
|
state,
|
|
27157
27156
|
move.moveInstance.rule,
|
|
@@ -27194,7 +27193,7 @@ ${message}`);
|
|
|
27194
27193
|
}
|
|
27195
27194
|
function getPossibleMoves(bgioState, moves, moveBuilder) {
|
|
27196
27195
|
const { eliminatedMoves, stepIndex } = moveBuilder;
|
|
27197
|
-
const
|
|
27196
|
+
const _possibleMoveMeta = {};
|
|
27198
27197
|
const allClickable = /* @__PURE__ */ new Set();
|
|
27199
27198
|
Object.entries(moves).filter(([moveName]) => !eliminatedMoves.includes(moveName)).forEach(([moveName, move]) => {
|
|
27200
27199
|
const moveRule = resolveProperties(bgioState, { ...move.moveInstance.rule, moveName });
|
|
@@ -27212,10 +27211,10 @@ ${message}`);
|
|
|
27212
27211
|
const clickableForMove = new Set(
|
|
27213
27212
|
moveIsAllowed && moveSteps?.[stepIndex]?.getClickable(context) || []
|
|
27214
27213
|
);
|
|
27215
|
-
|
|
27214
|
+
_possibleMoveMeta[moveName] = { clickableForMove };
|
|
27216
27215
|
clickableForMove.forEach((entity) => allClickable.add(entity));
|
|
27217
27216
|
});
|
|
27218
|
-
return {
|
|
27217
|
+
return { _possibleMoveMeta, allClickable };
|
|
27219
27218
|
}
|
|
27220
27219
|
function isMoveCompleted(state, moves, remainingMoveEntries, stepIndex) {
|
|
27221
27220
|
return remainingMoveEntries.length === 1 && getSteps(state, moves[remainingMoveEntries[0][0]].moveInstance.rule).length === stepIndex + 1;
|