board-game-engine 0.0.2 → 0.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/board-game-engine.test.js +2 -41
- package/dist/board-game-engine.js +32556 -5074
- package/dist/board-game-engine.min.js +2 -1
- package/dist/board-game-engine.min.js.LICENSE.txt +14 -0
- package/package.json +6 -3
- package/patch-bgio.cjs +23 -0
- package/src/client/client.js +287 -0
- package/src/game-factory/bank/bank-slot.js +69 -0
- package/src/game-factory/bank/bank.js +114 -0
- package/src/game-factory/board.js +3 -0
- package/src/game-factory/condition/condition-factory.js +52 -0
- package/src/game-factory/condition/condition.js +39 -0
- package/src/game-factory/condition/contains-condition.js +21 -0
- package/src/game-factory/condition/contains-same-condition.js +27 -0
- package/src/game-factory/condition/evaluate-condition.js +18 -0
- package/src/game-factory/condition/every-condition.js +25 -0
- package/src/game-factory/condition/has-line-condition.js +14 -0
- package/src/game-factory/condition/in-line-condition.js +19 -0
- package/src/game-factory/condition/is-condition.js +23 -0
- package/src/game-factory/condition/is-full-condition.js +9 -0
- package/src/game-factory/condition/no-possible-moves-condition.js +14 -0
- package/src/game-factory/condition/not-condition.js +14 -0
- package/src/game-factory/condition/or-condition.js +15 -0
- package/src/game-factory/condition/position-condition.js +12 -0
- package/src/game-factory/condition/some-condition.js +24 -0
- package/src/game-factory/condition/would-condition.js +94 -0
- package/src/game-factory/entity.js +29 -0
- package/src/game-factory/expand-game-rules.js +276 -0
- package/src/game-factory/game-factory.js +239 -0
- package/src/game-factory/move/end-turn.js +7 -0
- package/src/game-factory/move/for-each.js +18 -0
- package/src/game-factory/move/index.js +7 -0
- package/src/game-factory/move/move-entity.js +16 -0
- package/src/game-factory/move/move-factory.js +89 -0
- package/src/game-factory/move/move.js +131 -0
- package/src/game-factory/move/pass-turn.js +10 -0
- package/src/game-factory/move/pass.js +7 -0
- package/src/game-factory/move/place-new.js +33 -0
- package/src/game-factory/move/remove-entity.js +7 -0
- package/src/game-factory/move/set-active-players.js +23 -0
- package/src/game-factory/move/set-state.js +13 -0
- package/src/game-factory/move/shuffle.js +7 -0
- package/src/game-factory/move/take-from.js +7 -0
- package/src/game-factory/space/space.js +30 -0
- package/src/game-factory/space-group/grid.js +43 -0
- package/src/game-factory/space-group/space-group.js +29 -0
- package/src/index.js +2 -0
- package/src/registry.js +17 -0
- package/src/utils/any-valid-moves.js +157 -0
- package/src/utils/check-conditions.js +28 -0
- package/src/utils/create-payload.js +16 -0
- package/src/utils/deserialize-bgio-arguments.js +8 -0
- package/src/utils/do-moves.js +18 -0
- package/src/utils/entity-matches.js +20 -0
- package/src/utils/find-met-condition.js +22 -0
- package/src/utils/get-current-moves.js +12 -0
- package/src/utils/get-scenario-results.js +23 -0
- package/src/utils/get-steps.js +28 -0
- package/src/utils/get.js +25 -0
- package/src/utils/grid-contains-sequence.js +226 -0
- package/src/utils/json-transformer.js +12 -0
- package/src/utils/prepare-payload.js +16 -0
- package/src/utils/resolve-entity.js +9 -0
- package/src/utils/resolve-expression.js +10 -0
- package/src/utils/resolve-properties.js +157 -0
- package/src/utils/simulate-move.js +25 -0
- package/webpack.config.js +4 -1
- package/src/action/action-factory.js +0 -13
- package/src/action/action.js +0 -34
- package/src/action/move-piece-action.js +0 -11
- package/src/action/select-piece-action.js +0 -23
- package/src/action/swap-action.js +0 -14
- package/src/board/board-factory.js +0 -12
- package/src/board/board-group.js +0 -9
- package/src/board/board.js +0 -11
- package/src/board/grid.js +0 -52
- package/src/board/stack.js +0 -16
- package/src/condition/action-type-matches-condition.js +0 -7
- package/src/condition/bingo-condition.js +0 -50
- package/src/condition/blackout-condition.js +0 -9
- package/src/condition/condition-factory.js +0 -31
- package/src/condition/condition.js +0 -9
- package/src/condition/contains-condition.js +0 -14
- package/src/condition/does-not-contain-condition.js +0 -15
- package/src/condition/is-valid-player-condition.js +0 -7
- package/src/condition/piece-matches-condition.js +0 -23
- package/src/condition/relative-move-condition.js +0 -16
- package/src/condition/some-condition.js +0 -7
- package/src/game/game.ts +0 -362
- package/src/index.ts +0 -1
- package/src/piece/piece-factory.js +0 -5
- package/src/piece/piece.ts +0 -25
- package/src/piece/pile.js +0 -70
- package/src/player/player.ts +0 -13
- package/src/registry.ts +0 -51
- package/src/round/round-factory.js +0 -7
- package/src/round/round.js +0 -41
- package/src/round/sequential-player-turn.js +0 -18
- package/src/space/space.ts +0 -22
- package/src/utils/find-value-path.js +0 -37
- package/src/utils/resolve-board.ts +0 -38
- package/src/utils/resolve-piece.ts +0 -43
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*! *****************************************************************************
|
|
2
|
+
Copyright (c) Microsoft Corporation.
|
|
3
|
+
|
|
4
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
5
|
+
purpose with or without fee is hereby granted.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
8
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
9
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
10
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
11
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
12
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
13
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
14
|
+
***************************************************************************** */
|
package/package.json
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "board-game-engine",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "wip board game engine",
|
|
5
5
|
"main": "dist/board-game-engine.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "jest --coverage --verbose false",
|
|
8
8
|
"lint": "eslint .",
|
|
9
|
+
"postinstall": "node ./patch-bgio.cjs",
|
|
9
10
|
"build": "webpack",
|
|
10
11
|
"prepublish": "npm run build"
|
|
11
12
|
},
|
|
12
13
|
"author": "Matthew Broatch",
|
|
13
14
|
"license": "ISC",
|
|
14
15
|
"devDependencies": {
|
|
15
|
-
"@babel/preset-env": "^7.16.
|
|
16
|
+
"@babel/preset-env": "^7.16.8",
|
|
16
17
|
"@babel/preset-typescript": "^7.27.1",
|
|
17
18
|
"babel-jest": "^27.4.4",
|
|
18
19
|
"babel-loader": "",
|
|
@@ -23,10 +24,12 @@
|
|
|
23
24
|
"eslint-plugin-import": "^2.3.0",
|
|
24
25
|
"jest": "^27.4.3",
|
|
25
26
|
"standard": "^16.0.4",
|
|
26
|
-
"webpack": "^5.
|
|
27
|
+
"webpack": "^5.66.0",
|
|
27
28
|
"webpack-cli": "^4.9.1"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
31
|
+
"boardgame.io": "^0.50.2",
|
|
32
|
+
"expr-eval": "^2.0.2",
|
|
30
33
|
"wackson": "^1.1.0"
|
|
31
34
|
},
|
|
32
35
|
"repository": {
|
package/patch-bgio.cjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// scalpel to make guts.js work
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const serverFile = path.join(
|
|
6
|
+
__dirname,
|
|
7
|
+
'node_modules',
|
|
8
|
+
'boardgame.io',
|
|
9
|
+
'dist/cjs/server.js'
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
fs.appendFileSync(
|
|
13
|
+
serverFile,
|
|
14
|
+
`
|
|
15
|
+
exports.configureRouter = configureRouter
|
|
16
|
+
exports.createServerRunConfig = createServerRunConfig
|
|
17
|
+
exports.configureApp = configureApp
|
|
18
|
+
exports.getPortFromServer = getPortFromServer
|
|
19
|
+
exports.getPortFromServer = getPortFromServer
|
|
20
|
+
exports.Master = Master
|
|
21
|
+
exports.TransportAPI = TransportAPI
|
|
22
|
+
`
|
|
23
|
+
);
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { Client as BoardgameIOClient } from 'boardgame.io/client'
|
|
2
|
+
import { Debug } from 'boardgame.io/debug';
|
|
3
|
+
import { SocketIO } from 'boardgame.io/multiplayer'
|
|
4
|
+
import { serialize, deserialize } from 'wackson';
|
|
5
|
+
|
|
6
|
+
import gameFactory from '../game-factory/game-factory.js'
|
|
7
|
+
import { registry } from '../registry.js';
|
|
8
|
+
|
|
9
|
+
import simulateMove from '../utils/simulate-move.js';
|
|
10
|
+
import getCurrentMoves from '../utils/get-current-moves.js';
|
|
11
|
+
import resolveProperties from '../utils/resolve-properties.js';
|
|
12
|
+
import checkConditions from '../utils/check-conditions.js';
|
|
13
|
+
import preparePayload from '../utils/prepare-payload.js';
|
|
14
|
+
import getSteps from '../utils/get-steps.js';
|
|
15
|
+
import createPayload from '../utils/create-payload.js';
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
export default class Client {
|
|
19
|
+
constructor (options) {
|
|
20
|
+
this.options = options
|
|
21
|
+
this.moveBuilder = {
|
|
22
|
+
targets: [],
|
|
23
|
+
stepIndex: 0,
|
|
24
|
+
eliminatedMoves: []
|
|
25
|
+
}
|
|
26
|
+
this.optimisticWinner = null
|
|
27
|
+
this.allClickable = new Set()
|
|
28
|
+
this.game = gameFactory(JSON.parse(options.gameRules), options.gameName)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
connect () {
|
|
32
|
+
const {
|
|
33
|
+
server,
|
|
34
|
+
numPlayers,
|
|
35
|
+
onClientUpdate,
|
|
36
|
+
debug = {
|
|
37
|
+
collapseOnLoad: true,
|
|
38
|
+
impl: Debug,
|
|
39
|
+
},
|
|
40
|
+
gameId,
|
|
41
|
+
gameRules,
|
|
42
|
+
boardgamePlayerID,
|
|
43
|
+
clientToken,
|
|
44
|
+
singlePlayer = !clientToken,
|
|
45
|
+
} = this.options
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const clientOptions = singlePlayer
|
|
49
|
+
? { game: this.game, numPlayers, debug }
|
|
50
|
+
: {
|
|
51
|
+
game: this.game,
|
|
52
|
+
multiplayer: singlePlayer ? undefined : SocketIO({
|
|
53
|
+
server,
|
|
54
|
+
socketOpts: {
|
|
55
|
+
transports: ['websocket', 'polling']
|
|
56
|
+
}
|
|
57
|
+
}),
|
|
58
|
+
matchID: gameId,
|
|
59
|
+
playerID: boardgamePlayerID,
|
|
60
|
+
credentials: clientToken,
|
|
61
|
+
debug,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.client = BoardgameIOClient(clientOptions)
|
|
65
|
+
|
|
66
|
+
if (onClientUpdate) {
|
|
67
|
+
this.client.subscribe(onClientUpdate)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.client.start()
|
|
71
|
+
|
|
72
|
+
return this
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Failed to join game:', error)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getState () {
|
|
79
|
+
let state
|
|
80
|
+
let moves
|
|
81
|
+
let gameover
|
|
82
|
+
const clientState = this.client?.getState()
|
|
83
|
+
if (clientState) {
|
|
84
|
+
state = {
|
|
85
|
+
...clientState,
|
|
86
|
+
G: deserialize(JSON.stringify(clientState.G), registry),
|
|
87
|
+
originalG: clientState.G,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
gameover = state?.ctx?.gameover
|
|
91
|
+
|
|
92
|
+
// Fix: use arrow function so `this` refers to the Client instance
|
|
93
|
+
moves = !gameover
|
|
94
|
+
? Object.entries(getCurrentMoves(state, this.client)).reduce((acc, [moveName, rawMove]) => {
|
|
95
|
+
const move = (payload) => {
|
|
96
|
+
this.client.moves[moveName](preparePayload(payload))
|
|
97
|
+
}
|
|
98
|
+
move.moveInstance = rawMove.moveInstance
|
|
99
|
+
return {
|
|
100
|
+
...acc,
|
|
101
|
+
[moveName]: move
|
|
102
|
+
}
|
|
103
|
+
}, {})
|
|
104
|
+
: []
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
state,
|
|
109
|
+
gameover,
|
|
110
|
+
moves
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
doStep (target, isSpectator) {
|
|
115
|
+
const { state, moves } = this.getState()
|
|
116
|
+
const { possibleMoveMeta, allClickable } = getPossibleMoves(
|
|
117
|
+
state,
|
|
118
|
+
moves,
|
|
119
|
+
this.moveBuilder,
|
|
120
|
+
isSpectator
|
|
121
|
+
)
|
|
122
|
+
this.allClickable = allClickable
|
|
123
|
+
|
|
124
|
+
// Filter out moves that don't accept this target
|
|
125
|
+
const newEliminated = Object.entries(possibleMoveMeta)
|
|
126
|
+
.filter(([_, meta]) => !meta.finishedOnLastStep && !meta.clickableForMove.has(target))
|
|
127
|
+
.map(([name]) => name)
|
|
128
|
+
.concat(this.moveBuilder.eliminatedMoves);
|
|
129
|
+
|
|
130
|
+
if (newEliminated.length === Object.keys(moves).length) {
|
|
131
|
+
console.error('invalid move with target:', target?.rule);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.moveBuilder = {
|
|
136
|
+
eliminatedMoves: newEliminated,
|
|
137
|
+
stepIndex: this.moveBuilder.stepIndex + 1,
|
|
138
|
+
targets: [...this.moveBuilder.targets, target]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Fix: filter possibleMoveMeta to only include moves not in newEliminated,
|
|
142
|
+
// so findCompletedMove sees the post-elimination state rather than the stale pre-elimination state
|
|
143
|
+
const filteredMoveMeta = Object.fromEntries(
|
|
144
|
+
Object.entries(possibleMoveMeta).filter(([name]) => !newEliminated.includes(name))
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const completed = findCompletedMove(state, filteredMoveMeta, this.moveBuilder, moves);
|
|
148
|
+
|
|
149
|
+
if (completed) {
|
|
150
|
+
// without this, an extra post-game turn would flash
|
|
151
|
+
this.optimisticWinner = getWinnerAfterMove(
|
|
152
|
+
state,
|
|
153
|
+
this.game,
|
|
154
|
+
completed.move.moveInstance,
|
|
155
|
+
completed.payload
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
completed.move(completed.payload);
|
|
159
|
+
|
|
160
|
+
this.moveBuilder = ({ targets: [], stepIndex: 0, eliminatedMoves: [] });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.options.onClientUpdate?.()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
reset () {
|
|
167
|
+
this.moveBuilder = ({ targets: [], stepIndex: 0, eliminatedMoves: [] });
|
|
168
|
+
this.optimisticWinner = null
|
|
169
|
+
this.options.onClientUpdate?.()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
undoStep () {
|
|
173
|
+
if (this.moveBuilder.targets.length) {
|
|
174
|
+
this.moveBuilder = {
|
|
175
|
+
targets: this.moveBuilder.targets.slice(0, -1),
|
|
176
|
+
stepIndex: Math.max(0, this.moveBuilder.stepIndex - 1),
|
|
177
|
+
eliminatedMoves: []
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
this.options.onClientUpdate?.()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getPossibleMoves(bgioState, moves, moveBuilder, isSpectator) {
|
|
185
|
+
if (isSpectator) {
|
|
186
|
+
return { possibleMoveMeta: {}, allClickable: new Set() };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { eliminatedMoves, stepIndex } = moveBuilder;
|
|
190
|
+
|
|
191
|
+
const possibleMoveMeta = {};
|
|
192
|
+
const allClickable = new Set();
|
|
193
|
+
|
|
194
|
+
const availableMoves = Object.entries(moves)
|
|
195
|
+
.filter(([moveName]) => !eliminatedMoves.includes(moveName));
|
|
196
|
+
|
|
197
|
+
availableMoves.forEach(([moveName, move]) => {
|
|
198
|
+
const moveRule = resolveProperties(
|
|
199
|
+
bgioState,
|
|
200
|
+
{ ...move.moveInstance.rule, moveName }
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
const context = {
|
|
204
|
+
moveInstance: move.moveInstance,
|
|
205
|
+
moveArguments: moveRule.arguments
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const payload = createPayload(
|
|
209
|
+
bgioState,
|
|
210
|
+
moveRule,
|
|
211
|
+
moveBuilder.targets,
|
|
212
|
+
context
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
context.moveArguments = {
|
|
216
|
+
...context.moveArguments,
|
|
217
|
+
...payload.arguments,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const moveIsAllowed = checkConditions(
|
|
221
|
+
bgioState,
|
|
222
|
+
moveRule,
|
|
223
|
+
{},
|
|
224
|
+
context
|
|
225
|
+
).conditionsAreMet;
|
|
226
|
+
|
|
227
|
+
const moveSteps = getSteps(
|
|
228
|
+
bgioState,
|
|
229
|
+
moveRule
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const lastStep = moveSteps?.[stepIndex - 1];
|
|
233
|
+
const currentStep = moveSteps?.[stepIndex];
|
|
234
|
+
const finishedOnLastStep = moveSteps && !!lastStep && !currentStep;
|
|
235
|
+
|
|
236
|
+
const clickableForMove = new Set(
|
|
237
|
+
(moveIsAllowed && currentStep?.getClickable(context)) || []
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
possibleMoveMeta[moveName] = { finishedOnLastStep, clickableForMove };
|
|
241
|
+
|
|
242
|
+
clickableForMove.forEach((entity) => {
|
|
243
|
+
allClickable.add(entity);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return { possibleMoveMeta, allClickable };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Fix: renamed first parameter from `bgioArguments` to `bgioState` to match its actual usage
|
|
251
|
+
function findCompletedMove(bgioState, possibleMoveMeta, moveBuilder, moves) {
|
|
252
|
+
const possibleMoveNames = Object.keys(possibleMoveMeta);
|
|
253
|
+
|
|
254
|
+
// Only one possible move left
|
|
255
|
+
if (possibleMoveNames.length !== 1) return null;
|
|
256
|
+
|
|
257
|
+
const moveName = possibleMoveNames[0];
|
|
258
|
+
const meta = possibleMoveMeta[moveName];
|
|
259
|
+
|
|
260
|
+
// And it's finished
|
|
261
|
+
if (!meta.finishedOnLastStep) return null;
|
|
262
|
+
|
|
263
|
+
const move = moves[moveName];
|
|
264
|
+
const moveRule = move.moveInstance.rule;
|
|
265
|
+
|
|
266
|
+
const payload = createPayload(
|
|
267
|
+
bgioState,
|
|
268
|
+
moveRule,
|
|
269
|
+
moveBuilder.targets,
|
|
270
|
+
{ moveInstance: move.moveInstance }
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return { moveName, move, payload };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function getWinnerAfterMove (state, game, moveInstance, movePayload) {
|
|
277
|
+
const simulatedG = simulateMove(
|
|
278
|
+
state,
|
|
279
|
+
preparePayload(movePayload),
|
|
280
|
+
{ moveInstance }
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return game.endIf?.({
|
|
284
|
+
...state,
|
|
285
|
+
G: JSON.parse(serialize(simulatedG))
|
|
286
|
+
})
|
|
287
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import resolveProperties from '../../utils/resolve-properties.js'
|
|
2
|
+
|
|
3
|
+
// lazily create entities as needed, and also function as an index of entities created
|
|
4
|
+
class BankSlot {
|
|
5
|
+
constructor (rule, bank) {
|
|
6
|
+
this.bank = bank
|
|
7
|
+
this.rule = rule
|
|
8
|
+
this.pool = []
|
|
9
|
+
this.remaining = +rule.count || 1
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getOne (bgioArguments, options, context) {
|
|
13
|
+
return this.getMultiple(bgioArguments, 1, options, context)[0]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getMultiple (bgioArguments, count = Infinity, options = {}, context) {
|
|
17
|
+
const toReturn = []
|
|
18
|
+
|
|
19
|
+
if (this.remaining === Infinity && count === Infinity) {
|
|
20
|
+
throw new Error(`Cannot get infinite pieces from slot with infinite remaining: ${this.rule.name}`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (count !== Infinity && count > this.remaining) {
|
|
24
|
+
throw new Error(`Requested ${count} pieces but only ${this.remaining} available in slot: ${this.rule.name}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Determine actual count to fetch
|
|
28
|
+
const actualCount = count === Infinity ? this.remaining : count
|
|
29
|
+
|
|
30
|
+
if (this.remaining !== Infinity) {
|
|
31
|
+
this.remaining -= actualCount
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const fromPool = Math.min(actualCount, this.pool.length)
|
|
35
|
+
toReturn.push(...this.pool.splice(0, fromPool))
|
|
36
|
+
|
|
37
|
+
const remainder = actualCount - fromPool
|
|
38
|
+
if (remainder > 0) {
|
|
39
|
+
toReturn.push(
|
|
40
|
+
...Array.from(new Array(remainder)).map(() =>
|
|
41
|
+
this.bank.createEntity(this.rule)
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (options.state) {
|
|
47
|
+
const newState = resolveProperties(bgioArguments, options.state, context)
|
|
48
|
+
toReturn.forEach(entity => {
|
|
49
|
+
entity.state = { ...entity.state, ...newState }
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return toReturn
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
returnToBank (entity) {
|
|
57
|
+
if (entity.rule.state) {
|
|
58
|
+
entity.state = entity.rule.state
|
|
59
|
+
} else {
|
|
60
|
+
delete entity.state
|
|
61
|
+
}
|
|
62
|
+
if (this.remaining !== undefined) {
|
|
63
|
+
this.remaining += 1
|
|
64
|
+
}
|
|
65
|
+
this.pool.push(entity)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default BankSlot
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import find from 'lodash/find.js'
|
|
2
|
+
import filter from 'lodash/filter.js'
|
|
3
|
+
import checkConditions from '../../utils/check-conditions.js'
|
|
4
|
+
import { registry } from '../../registry.js'
|
|
5
|
+
import BankSlot from './bank-slot.js'
|
|
6
|
+
|
|
7
|
+
class Bank {
|
|
8
|
+
constructor (entityRules) {
|
|
9
|
+
this.currentEntityId = 0
|
|
10
|
+
this.tracker = {}
|
|
11
|
+
this.slots = entityRules.map(rule => new BankSlot(rule, this))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
createEntity (definition = {}, options) {
|
|
15
|
+
const entity = new (registry[definition.type || 'Entity'])(
|
|
16
|
+
{
|
|
17
|
+
bank: this,
|
|
18
|
+
fromBank: true,
|
|
19
|
+
...options
|
|
20
|
+
},
|
|
21
|
+
definition,
|
|
22
|
+
this.currentEntityId++
|
|
23
|
+
)
|
|
24
|
+
this.track(entity)
|
|
25
|
+
return entity
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
track (entity) {
|
|
29
|
+
this.tracker[entity.entityId] = entity
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
locate (entityId) {
|
|
33
|
+
return this.tracker[entityId]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
findAll (bgioArguments, rule, context) {
|
|
37
|
+
if (!rule.conditions) {
|
|
38
|
+
throw new Error (`Cannot find entity with no conditions. Rule: ${JSON.stringify(rule)}`)
|
|
39
|
+
}
|
|
40
|
+
return filter(
|
|
41
|
+
Object.values(this.tracker),
|
|
42
|
+
entity => checkConditions(
|
|
43
|
+
bgioArguments,
|
|
44
|
+
rule,
|
|
45
|
+
{ target: entity },
|
|
46
|
+
context
|
|
47
|
+
).conditionsAreMet
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
findOne (bgioArguments, rule, context) {
|
|
52
|
+
return this.findAll(bgioArguments, rule, context)[0]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
find (bgioArguments, rule, context) {
|
|
56
|
+
return rule.matchMultiple
|
|
57
|
+
? this.findAll(bgioArguments, rule, context)
|
|
58
|
+
: this.findOne(bgioArguments, rule, context)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
findParent (entity) {
|
|
62
|
+
return find(this.tracker, ent =>
|
|
63
|
+
ent.entities?.includes(entity)
|
|
64
|
+
|| ent.spaces?.includes(entity)
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getOne (bgioArguments, rule, context) {
|
|
69
|
+
const slot = this.getSlot(bgioArguments, rule, context)
|
|
70
|
+
if (!slot) {
|
|
71
|
+
console.error(`No matching slot for ${JSON.stringify(rule)}`)
|
|
72
|
+
}
|
|
73
|
+
return slot.getOne(bgioArguments, { state: rule.state }, context)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getMultiple (bgioArguments, rule, count, context) {
|
|
77
|
+
const slots = this.getSlots(bgioArguments, rule, context)
|
|
78
|
+
if (!slots.length) {
|
|
79
|
+
console.error(`No matching slots for ${JSON.stringify(rule)}`)
|
|
80
|
+
}
|
|
81
|
+
return slots.reduce((acc, slot) => [
|
|
82
|
+
...acc,
|
|
83
|
+
...slot.getMultiple(bgioArguments, count, { state: rule.state })
|
|
84
|
+
], [])
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getSlot (bgioArguments, rule, context) {
|
|
88
|
+
return this.slots.find(slot => checkConditions(
|
|
89
|
+
bgioArguments,
|
|
90
|
+
rule,
|
|
91
|
+
{ target: slot },
|
|
92
|
+
context
|
|
93
|
+
).conditionsAreMet
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getSlots (bgioArguments, rule, context) {
|
|
98
|
+
return this.slots.filter(slot => checkConditions(
|
|
99
|
+
bgioArguments,
|
|
100
|
+
rule,
|
|
101
|
+
{ target: slot },
|
|
102
|
+
context
|
|
103
|
+
).conditionsAreMet
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
returnToBank (bgioArguments, entity) {
|
|
108
|
+
this.findParent(entity).remove(entity)
|
|
109
|
+
this.getSlot(bgioArguments, entity.rule).returnToBank(entity)
|
|
110
|
+
delete this.tracker[entity.entityId]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default Bank
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import Is from "./is-condition.js";
|
|
2
|
+
import Not from "./not-condition.js";
|
|
3
|
+
import Or from "./or-condition.js";
|
|
4
|
+
import Some from "./some-condition.js";
|
|
5
|
+
import Every from "./every-condition.js";
|
|
6
|
+
import ContainsCondition from "./contains-condition.js";
|
|
7
|
+
import ContainsSameCondition from "./contains-same-condition.js";
|
|
8
|
+
import InLine from "./in-line-condition.js";
|
|
9
|
+
import HasLine from "./has-line-condition.js";
|
|
10
|
+
import IsFull from "./is-full-condition.js";
|
|
11
|
+
import Would from "./would-condition.js";
|
|
12
|
+
import NoPossibleMoves from "./no-possible-moves-condition.js";
|
|
13
|
+
import Evaluate from "./evaluate-condition.js";
|
|
14
|
+
import Position from "./position-condition.js";
|
|
15
|
+
// import BingoCondition from "./bingo-condition.js";
|
|
16
|
+
// import RelativeMoveCondition from "./relative-move-condition.js";
|
|
17
|
+
|
|
18
|
+
export default function conditionFactory(rule) {
|
|
19
|
+
if (rule.conditionType === "Is") {
|
|
20
|
+
return new Is(rule);
|
|
21
|
+
} else if (rule.conditionType === "Not") {
|
|
22
|
+
return new Not(rule);
|
|
23
|
+
} else if (rule.conditionType === "Or") {
|
|
24
|
+
return new Or(rule);
|
|
25
|
+
} else if (rule.conditionType === "Some") {
|
|
26
|
+
return new Some(rule);
|
|
27
|
+
} else if (rule.conditionType === "Contains") {
|
|
28
|
+
return new ContainsCondition(rule);
|
|
29
|
+
} else if (rule.conditionType === "ContainsSame") {
|
|
30
|
+
return new ContainsSameCondition(rule);
|
|
31
|
+
} else if (rule.conditionType === "Every") {
|
|
32
|
+
return new Every(rule);
|
|
33
|
+
} else if (rule.conditionType === "InLine") {
|
|
34
|
+
return new InLine(rule);
|
|
35
|
+
} else if (rule.conditionType === "HasLine") {
|
|
36
|
+
return new HasLine(rule);
|
|
37
|
+
} else if (rule.conditionType === "IsFull") {
|
|
38
|
+
return new IsFull(rule);
|
|
39
|
+
} else if (rule.conditionType === "Would") {
|
|
40
|
+
return new Would(rule);
|
|
41
|
+
} else if (rule.conditionType === "NoPossibleMoves") {
|
|
42
|
+
return new NoPossibleMoves(rule);
|
|
43
|
+
} else if (rule.conditionType === "Evaluate") {
|
|
44
|
+
return new Evaluate(rule);
|
|
45
|
+
} else if (rule.conditionType === "Position") {
|
|
46
|
+
return new Position(rule);
|
|
47
|
+
// } else if (rule.conditionType === "bingo") {
|
|
48
|
+
// return new BingoCondition(rule);
|
|
49
|
+
// } else if (rule.conditionType === "relativeMove") {
|
|
50
|
+
// return new RelativeMoveCondition(rule);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import resolveProperties from "../../utils/resolve-properties.js";
|
|
2
|
+
|
|
3
|
+
export default class Condition {
|
|
4
|
+
constructor (rule) {
|
|
5
|
+
this.rule = rule
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
check (bgioArguments, payload, context) {
|
|
9
|
+
const conditionPayload = { ...payload }
|
|
10
|
+
const newContext = { ...context }
|
|
11
|
+
|
|
12
|
+
if (conditionPayload.target) {
|
|
13
|
+
newContext.originalTarget = conditionPayload.target
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const rule = resolveProperties(
|
|
17
|
+
bgioArguments,
|
|
18
|
+
this.rule,
|
|
19
|
+
newContext
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
// We don't simply defer to payload target because of Parent and RelativePath
|
|
23
|
+
// target types, for instance, which retarget to another entity
|
|
24
|
+
if (rule.target !== undefined) {
|
|
25
|
+
conditionPayload.target = rule.target
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Nonexistent entities never fulfill conditions (including "Not" conditions!)
|
|
29
|
+
if (this.rule.target !== undefined && !conditionPayload.target) {
|
|
30
|
+
return { conditionIsMet: false }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return this.checkCondition(bgioArguments, rule, conditionPayload, newContext)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isMet(...args) {
|
|
37
|
+
return this.check(...args).conditionIsMet
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import _matches from "lodash/matches.js";
|
|
2
|
+
import Condition from "../condition/condition.js";
|
|
3
|
+
import checkConditions from "../../utils/check-conditions.js";
|
|
4
|
+
|
|
5
|
+
export default class ContainsCondition extends Condition {
|
|
6
|
+
checkCondition(bgioArguments, rule, payload, context) {
|
|
7
|
+
const { target } = payload
|
|
8
|
+
if (!target) {
|
|
9
|
+
return { matches: [], conditionIsMet: false }
|
|
10
|
+
} else {
|
|
11
|
+
const candidates = target.entities ?? target.spaces
|
|
12
|
+
const matches = candidates?.filter(entity => checkConditions(
|
|
13
|
+
bgioArguments,
|
|
14
|
+
rule,
|
|
15
|
+
{ target: entity },
|
|
16
|
+
context
|
|
17
|
+
).conditionsAreMet) ?? []
|
|
18
|
+
return { matches, conditionIsMet: !!matches.length }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pick from "lodash/pick.js";
|
|
2
|
+
import conditionFactory from "./condition-factory.js";
|
|
3
|
+
import Condition from "./condition.js";
|
|
4
|
+
|
|
5
|
+
export default class ContainsSame extends Condition {
|
|
6
|
+
checkCondition (bgioArguments, rule, { targets }) {
|
|
7
|
+
if (targets.length === 1 && targets[0].entities?.length) {
|
|
8
|
+
return { conditionIsMet: true }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const [ first, ...restEntities ] = targets;
|
|
12
|
+
const conditionIsMet = first.entities.some(entity => {
|
|
13
|
+
const condition = conditionFactory({
|
|
14
|
+
conditionType: "Contains",
|
|
15
|
+
conditions: [{
|
|
16
|
+
conditionType: 'Is',
|
|
17
|
+
matcher: pick(entity.rule, rule.properties)
|
|
18
|
+
}]
|
|
19
|
+
})
|
|
20
|
+
return restEntities.every(ent => {
|
|
21
|
+
return condition.isMet(bgioArguments, { target: ent })
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
return { conditionIsMet }
|
|
26
|
+
}
|
|
27
|
+
}
|