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,43 @@
|
|
|
1
|
+
import chunk from "lodash/chunk.js";
|
|
2
|
+
import SpaceGroup from "../space-group/space-group.js";
|
|
3
|
+
|
|
4
|
+
export default class Grid extends SpaceGroup {
|
|
5
|
+
getSpacesCount () {
|
|
6
|
+
return this.rule.width * this.rule.height
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
getRows () {
|
|
10
|
+
return chunk(this.spaces, this.rule.width)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getCoordinates (index) {
|
|
14
|
+
const { width } = this.rule
|
|
15
|
+
return [
|
|
16
|
+
index % width,
|
|
17
|
+
Math.floor(index/width)
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getIndex ([ x, y ]) {
|
|
22
|
+
const { width } = this.rule
|
|
23
|
+
return y * width + x
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getSpace (coordinates) {
|
|
27
|
+
return this.spaces[this.getIndex(coordinates)]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getRelativeCoordinates ([oldX, oldY], [relativeX, relativeY]) {
|
|
31
|
+
const newCoordinates = [oldX + relativeX, oldY + relativeY]
|
|
32
|
+
return this.areCoordinatesValid(newCoordinates)
|
|
33
|
+
? newCoordinates
|
|
34
|
+
: null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
areCoordinatesValid([x, y]) {
|
|
38
|
+
return x >= 0
|
|
39
|
+
&& y >= 0
|
|
40
|
+
&& x < this.rule.width
|
|
41
|
+
&& y < this.rule.height
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import Entity from "../entity.js";
|
|
2
|
+
|
|
3
|
+
export default class SpaceGroup extends Entity {
|
|
4
|
+
constructor (options, ...rest) {
|
|
5
|
+
super(options, ...rest)
|
|
6
|
+
this.spaces = this.makeSpaces(options.bank);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
makeSpaces (bank) {
|
|
10
|
+
return Array(this.getSpacesCount()).fill()
|
|
11
|
+
.map((_, i) => bank.createEntity({ type: 'Space', index: i }))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getEmptySpaces() {
|
|
15
|
+
return this.spaces.filter(space => space.isEmpty())
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getSpace(index) {
|
|
19
|
+
return this.spaces[index]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getEntities(index) {
|
|
23
|
+
return this.getSpace(index).entities;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
placeEntity(index, entity) {
|
|
27
|
+
this.getSpace(index).placeEntity(entity);
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.js
ADDED
package/src/registry.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Board from "./game-factory/board.js";
|
|
2
|
+
import SpaceGroup from "./game-factory/space-group/space-group.js";
|
|
3
|
+
import Space from "./game-factory/space/space.js";
|
|
4
|
+
import Grid from "./game-factory/space-group/grid.js";
|
|
5
|
+
import Bank from "./game-factory/bank/bank.js";
|
|
6
|
+
import BankSlot from "./game-factory/bank/bank-slot.js";
|
|
7
|
+
import Entity from "./game-factory/entity.js";
|
|
8
|
+
|
|
9
|
+
export const registry = {
|
|
10
|
+
Board,
|
|
11
|
+
SpaceGroup,
|
|
12
|
+
Space,
|
|
13
|
+
Grid,
|
|
14
|
+
Bank,
|
|
15
|
+
BankSlot,
|
|
16
|
+
Entity,
|
|
17
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import isPlainObject from "lodash/isPlainObject.js";
|
|
2
|
+
import resolveProperties from "./resolve-properties.js";
|
|
3
|
+
import resolveEntity from "./resolve-entity.js";
|
|
4
|
+
|
|
5
|
+
// Recursively find all contextPath references to moveArguments
|
|
6
|
+
function findMoveArgumentReferences(obj, refs = new Set()) {
|
|
7
|
+
if (!obj || typeof obj !== 'object') {
|
|
8
|
+
return refs;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Check if this is a contextPath reference to moveArguments
|
|
12
|
+
if (obj.type === 'contextPath' && Array.isArray(obj.path)) {
|
|
13
|
+
if (obj.path[0] === 'moveArguments' && obj.path[1]) {
|
|
14
|
+
refs.add(obj.path[1]);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Recurse into object properties and array elements
|
|
19
|
+
for (const value of Object.values(obj)) {
|
|
20
|
+
findMoveArgumentReferences(value, refs);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return refs;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Build a dependency graph and return topologically sorted argument names
|
|
27
|
+
function getArgumentOrder(ruleArguments) {
|
|
28
|
+
const argNames = Object.keys(ruleArguments);
|
|
29
|
+
const graph = {};
|
|
30
|
+
const inDegree = {};
|
|
31
|
+
|
|
32
|
+
// Initialize
|
|
33
|
+
argNames.forEach(name => {
|
|
34
|
+
graph[name] = [];
|
|
35
|
+
inDegree[name] = 0;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Build dependency edges (if arg B references arg A, A -> B)
|
|
39
|
+
argNames.forEach(argName => {
|
|
40
|
+
const arg = ruleArguments[argName];
|
|
41
|
+
const referencedArgs = findMoveArgumentReferences(arg);
|
|
42
|
+
|
|
43
|
+
referencedArgs.forEach(refArg => {
|
|
44
|
+
if (argNames.includes(refArg) && refArg !== argName) {
|
|
45
|
+
graph[refArg].push(argName);
|
|
46
|
+
inDegree[argName]++;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Topological sort (Kahn's algorithm)
|
|
52
|
+
const queue = argNames.filter(name => inDegree[name] === 0);
|
|
53
|
+
const sorted = [];
|
|
54
|
+
|
|
55
|
+
while (queue.length > 0) {
|
|
56
|
+
const current = queue.shift();
|
|
57
|
+
sorted.push(current);
|
|
58
|
+
|
|
59
|
+
graph[current].forEach(neighbor => {
|
|
60
|
+
inDegree[neighbor]--;
|
|
61
|
+
if (inDegree[neighbor] === 0) {
|
|
62
|
+
queue.push(neighbor);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// If not all nodes processed, there's a cycle - fall back to original order
|
|
68
|
+
return sorted.length === argNames.length ? sorted : argNames;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Recursively try to build a valid argument combination
|
|
72
|
+
function findValidCombination(
|
|
73
|
+
bgioArguments,
|
|
74
|
+
moveInstance,
|
|
75
|
+
ruleArguments,
|
|
76
|
+
orderedArgNames,
|
|
77
|
+
context,
|
|
78
|
+
index = 0,
|
|
79
|
+
currentArgs = {}
|
|
80
|
+
) {
|
|
81
|
+
// Base case: all arguments resolved
|
|
82
|
+
if (index === orderedArgNames.length) {
|
|
83
|
+
const resolvedPayload = { arguments: currentArgs };
|
|
84
|
+
return moveInstance.isValid(bgioArguments, resolvedPayload, context);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const argName = orderedArgNames[index];
|
|
88
|
+
const arg = ruleArguments[argName];
|
|
89
|
+
|
|
90
|
+
// Update context with current arguments for dependency resolution
|
|
91
|
+
const updatedContext = {
|
|
92
|
+
...context,
|
|
93
|
+
moveArguments: currentArgs
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Get all possible values for this argument if not resolved
|
|
97
|
+
// If it is unresolved, it means it was a playerChoice
|
|
98
|
+
const matches = isPlainObject(arg)
|
|
99
|
+
? resolveEntity(
|
|
100
|
+
bgioArguments,
|
|
101
|
+
{ ...arg, matchMultiple: true },
|
|
102
|
+
updatedContext,
|
|
103
|
+
argName
|
|
104
|
+
)
|
|
105
|
+
: arg;
|
|
106
|
+
|
|
107
|
+
const matchArray = Array.isArray(matches) ? matches : (matches !== undefined ? [matches] : []);
|
|
108
|
+
|
|
109
|
+
// If no valid values for this argument, this branch fails
|
|
110
|
+
if (matchArray.length === 0) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Try each possible value (short-circuits on first success)
|
|
115
|
+
return matchArray.some(value => {
|
|
116
|
+
return findValidCombination(
|
|
117
|
+
bgioArguments,
|
|
118
|
+
moveInstance,
|
|
119
|
+
ruleArguments,
|
|
120
|
+
orderedArgNames,
|
|
121
|
+
context,
|
|
122
|
+
index + 1,
|
|
123
|
+
{ ...currentArgs, [argName]: value }
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default function areThereValidMoves(bgioArguments, moves) {
|
|
129
|
+
return Object.values(moves).some(move => {
|
|
130
|
+
const { moveInstance } = move;
|
|
131
|
+
const context = { moveInstance };
|
|
132
|
+
const rule = resolveProperties(
|
|
133
|
+
bgioArguments,
|
|
134
|
+
moveInstance.rule,
|
|
135
|
+
context
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const ruleArguments = rule.arguments ?? {};
|
|
139
|
+
|
|
140
|
+
// If no arguments required, just check if move is valid
|
|
141
|
+
if (Object.keys(ruleArguments).length === 0) {
|
|
142
|
+
return moveInstance.isValid(bgioArguments, { arguments: {} }, context);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Get dependency-ordered argument names
|
|
146
|
+
const orderedArgNames = getArgumentOrder(ruleArguments);
|
|
147
|
+
|
|
148
|
+
// Recursively search for any valid combination (short-circuits on first valid)
|
|
149
|
+
return findValidCombination(
|
|
150
|
+
bgioArguments,
|
|
151
|
+
moveInstance,
|
|
152
|
+
ruleArguments,
|
|
153
|
+
orderedArgNames,
|
|
154
|
+
context
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import conditionFactory from '../game-factory/condition/condition-factory.js';
|
|
2
|
+
|
|
3
|
+
export default function checkConditions (
|
|
4
|
+
bgioArguments,
|
|
5
|
+
rule,
|
|
6
|
+
payload,
|
|
7
|
+
context
|
|
8
|
+
) {
|
|
9
|
+
const { conditions = [] } = rule
|
|
10
|
+
const results = [];
|
|
11
|
+
let failedAt
|
|
12
|
+
for (const conditionRule of conditions) {
|
|
13
|
+
const result = conditionFactory(conditionRule)
|
|
14
|
+
.check(bgioArguments, payload, context);
|
|
15
|
+
if (!result.conditionIsMet) {
|
|
16
|
+
failedAt = conditionRule
|
|
17
|
+
break
|
|
18
|
+
} else {
|
|
19
|
+
results.push(result);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
results,
|
|
25
|
+
failedAt,
|
|
26
|
+
conditionsAreMet: results.length === conditions.length
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import getSteps from './get-steps.js';
|
|
2
|
+
|
|
3
|
+
export default function createPayload (bgioState, moveRule, targets, context) {
|
|
4
|
+
const argNames = getSteps(
|
|
5
|
+
bgioState,
|
|
6
|
+
moveRule,
|
|
7
|
+
context
|
|
8
|
+
).map(s => s.argName)
|
|
9
|
+
return {
|
|
10
|
+
arguments: targets.reduce((acc, target, i) => ({
|
|
11
|
+
...acc,
|
|
12
|
+
[argNames[i]]: target
|
|
13
|
+
}), {})
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import moveFactory from "../game-factory/move/move-factory.js";
|
|
2
|
+
|
|
3
|
+
export default function doMoves (bgioArguments, moves = [], context) {
|
|
4
|
+
if (!moves?.length) {
|
|
5
|
+
return bgioArguments.G
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
moves.forEach((moveRule) => {
|
|
9
|
+
moveFactory(moveRule, context.game).moveInstance.doMove(
|
|
10
|
+
bgioArguments,
|
|
11
|
+
undefined,
|
|
12
|
+
context
|
|
13
|
+
);
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
return bgioArguments.G
|
|
17
|
+
}
|
|
18
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import matches from 'lodash/matches.js'
|
|
2
|
+
import resolveProperties from '../utils/resolve-properties.js'
|
|
3
|
+
|
|
4
|
+
function resolveMatcher (bgioArguments, matcher, context) {
|
|
5
|
+
const resolvedMatcher = { ...matcher }
|
|
6
|
+
delete resolvedMatcher.state
|
|
7
|
+
delete resolvedMatcher.stateGroups
|
|
8
|
+
return resolveProperties(bgioArguments, resolvedMatcher, context)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getEntityMatcher (entity) {
|
|
12
|
+
return {
|
|
13
|
+
...entity.rule,
|
|
14
|
+
...entity.state
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function entityMatches (bgioArguments, matcher, entity, context) {
|
|
19
|
+
return matches(resolveMatcher(bgioArguments, matcher, context))(getEntityMatcher(entity))
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import conditionFactory from '../game-factory/condition/condition-factory.js';
|
|
2
|
+
|
|
3
|
+
export default function findMetCondition (
|
|
4
|
+
bgioArguments,
|
|
5
|
+
{ conditions = [] },
|
|
6
|
+
payload,
|
|
7
|
+
context,
|
|
8
|
+
) {
|
|
9
|
+
let success
|
|
10
|
+
for (const conditionRule of conditions) {
|
|
11
|
+
const result = conditionFactory(conditionRule)
|
|
12
|
+
.check(bgioArguments, payload, context);
|
|
13
|
+
if (result.conditionIsMet) {
|
|
14
|
+
success = {
|
|
15
|
+
...result,
|
|
16
|
+
conditionRule
|
|
17
|
+
}
|
|
18
|
+
break
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return success
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// get the most specific set of moves for current stage/phase
|
|
2
|
+
// this will probably all break for complex stages with multiple active players
|
|
3
|
+
export default function getCurrentMoves (state, { game, playerID, stageName }) {
|
|
4
|
+
const phaseName = state.ctx.phase
|
|
5
|
+
|
|
6
|
+
// currentPlayer used for single player editor mode
|
|
7
|
+
const stageNameToUse = stageName ?? state.ctx.activePlayers?.[playerID ?? state.ctx.currentPlayer]
|
|
8
|
+
const phaseOrRoot = game.phases?.[phaseName] ?? game
|
|
9
|
+
const stageOrPhaseOrRoot = phaseOrRoot.turn?.stages?.[stageNameToUse] ?? phaseOrRoot
|
|
10
|
+
|
|
11
|
+
return stageOrPhaseOrRoot.moves ?? {}
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import checkConditions from "./check-conditions.js";
|
|
2
|
+
import resolveProperties from './resolve-properties.js'
|
|
3
|
+
|
|
4
|
+
export default function getScenarioResults(bgioArguments, scenarios, context) {
|
|
5
|
+
let match
|
|
6
|
+
for (const scenario of scenarios) {
|
|
7
|
+
const conditionResults = checkConditions(bgioArguments, scenario)
|
|
8
|
+
if (conditionResults.conditionsAreMet) {
|
|
9
|
+
match = { scenario, conditionResults }
|
|
10
|
+
break
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (match?.scenario?.result) {
|
|
15
|
+
return resolveProperties(
|
|
16
|
+
bgioArguments,
|
|
17
|
+
match.scenario.result,
|
|
18
|
+
{ results: match.conditionResults.results }
|
|
19
|
+
)
|
|
20
|
+
} else {
|
|
21
|
+
return match
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// controls order of what players need to click first
|
|
2
|
+
const argNamesMap = {
|
|
3
|
+
PlaceNew: ['destination'],
|
|
4
|
+
RemoveEntity: ['entity'],
|
|
5
|
+
MoveEntity: ['entity', 'destination'],
|
|
6
|
+
TakeFrom: ['source', 'destination'],
|
|
7
|
+
SetState: ['entity', 'state'],
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// this might not be where special handling for setstate wants to live
|
|
11
|
+
export default function getSteps (bgioState, moveRule) {
|
|
12
|
+
return argNamesMap[moveRule.type]
|
|
13
|
+
.filter(argName => moveRule.arguments[argName].playerChoice)
|
|
14
|
+
.map(argName => ({
|
|
15
|
+
argName,
|
|
16
|
+
getClickable: argName === 'state'
|
|
17
|
+
? () => moveRule.arguments[argName].possibleValues.map((value) => ({
|
|
18
|
+
abstract: true,
|
|
19
|
+
...moveRule.arguments[argName],
|
|
20
|
+
value
|
|
21
|
+
}))
|
|
22
|
+
: (context) => bgioState.G.bank.findAll(
|
|
23
|
+
bgioState,
|
|
24
|
+
moveRule.arguments[argName],
|
|
25
|
+
context
|
|
26
|
+
)
|
|
27
|
+
}))
|
|
28
|
+
}
|
package/src/utils/get.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export default function get (obj, pathArray) {
|
|
2
|
+
let current = obj;
|
|
3
|
+
|
|
4
|
+
for (const step of pathArray) {
|
|
5
|
+
if (current === undefined) {
|
|
6
|
+
return current
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (step?.flatten) {
|
|
10
|
+
if (!Array.isArray(current)) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
current = current.flat();
|
|
15
|
+
|
|
16
|
+
if (step.map) {
|
|
17
|
+
current = current.map(item => get(item, step.map));
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
current = current[step];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return current;
|
|
25
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// claude ai did most of this
|
|
2
|
+
import _matches from "lodash/matches.js";
|
|
3
|
+
import checkConditions from "./check-conditions.js";
|
|
4
|
+
|
|
5
|
+
// We'll check reverse directions along each line
|
|
6
|
+
const directions = [
|
|
7
|
+
[1, 0], // horizontal
|
|
8
|
+
[0, 1], // vertical
|
|
9
|
+
[1, 1], // diagonal down-right
|
|
10
|
+
[-1, 1], // diagonal down-left
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const sequenceCache = new WeakMap();
|
|
14
|
+
|
|
15
|
+
function getSequenceKey(sequencePattern, context) {
|
|
16
|
+
const contextKey = {
|
|
17
|
+
moveInstance: context.moveInstance?.id,
|
|
18
|
+
moveArguments: context.moveArguments,
|
|
19
|
+
// Add other context properties that conditions might use
|
|
20
|
+
};
|
|
21
|
+
return JSON.stringify({ pattern: sequencePattern, context: contextKey });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// todo: use stable hash library that we're using for game rules hash
|
|
25
|
+
function getGridStateKey(grid) {
|
|
26
|
+
const spaces = grid.entities || [];
|
|
27
|
+
|
|
28
|
+
return spaces.map(space => {
|
|
29
|
+
const entities = space.entities || [];
|
|
30
|
+
if (entities.length === 0) return 'empty';
|
|
31
|
+
|
|
32
|
+
return entities.map(entity => {
|
|
33
|
+
const sortedKeys = Object.keys(entity).sort();
|
|
34
|
+
const stateObj = {};
|
|
35
|
+
sortedKeys.forEach(key => {
|
|
36
|
+
stateObj[key] = entity[key];
|
|
37
|
+
});
|
|
38
|
+
return JSON.stringify(stateObj);
|
|
39
|
+
}).sort().join('|');
|
|
40
|
+
}).join(',');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findSequencesInLine(bgioArguments, lineSpaces, sequencePattern, minSequenceLength, context, reverse = false) {
|
|
44
|
+
const matches = [];
|
|
45
|
+
|
|
46
|
+
// Use original array or iterate in reverse without creating new array
|
|
47
|
+
const length = lineSpaces.length;
|
|
48
|
+
let startIndex = 0;
|
|
49
|
+
|
|
50
|
+
while (startIndex <= length - minSequenceLength) {
|
|
51
|
+
const matchedSpaces = tryMatchSequence(
|
|
52
|
+
bgioArguments,
|
|
53
|
+
lineSpaces,
|
|
54
|
+
startIndex,
|
|
55
|
+
sequencePattern,
|
|
56
|
+
context,
|
|
57
|
+
reverse
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (matchedSpaces) {
|
|
61
|
+
matches.push(matchedSpaces);
|
|
62
|
+
startIndex++; // Move one space forward to find overlapping matches
|
|
63
|
+
} else {
|
|
64
|
+
startIndex++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return matches;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getLineStartingPoints(grid, dx, dy) {
|
|
72
|
+
const { width, height } = grid.attributes;
|
|
73
|
+
const starts = [];
|
|
74
|
+
|
|
75
|
+
if (dx === 1 && dy === 0) {
|
|
76
|
+
// Horizontal: start at leftmost column
|
|
77
|
+
for (let y = 0; y < height; y++) starts.push([0, y]);
|
|
78
|
+
} else if (dx === 0 && dy === 1) {
|
|
79
|
+
// Vertical: start at top row
|
|
80
|
+
for (let x = 0; x < width; x++) starts.push([x, 0]);
|
|
81
|
+
} else if (dx === 1 && dy === 1) {
|
|
82
|
+
// Diagonal down-right: start from top row and left column
|
|
83
|
+
for (let x = 0; x < width; x++) starts.push([x, 0]);
|
|
84
|
+
for (let y = 1; y < height; y++) starts.push([0, y]);
|
|
85
|
+
} else if (dx === -1 && dy === 1) {
|
|
86
|
+
// Diagonal down-left: start from top row and right column
|
|
87
|
+
for (let x = 0; x < width; x++) starts.push([x, 0]);
|
|
88
|
+
for (let y = 1; y < height; y++) starts.push([width - 1, y]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return starts;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getLineSpaces(grid, startX, startY, dx, dy) {
|
|
95
|
+
const spaces = [];
|
|
96
|
+
let [x, y] = [startX, startY];
|
|
97
|
+
|
|
98
|
+
while (grid.areCoordinatesValid([x, y])) {
|
|
99
|
+
spaces.push(grid.getSpace([x, y]));
|
|
100
|
+
x += dx;
|
|
101
|
+
y += dy;
|
|
102
|
+
}
|
|
103
|
+
return spaces;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function tryMatchSequence(bgioArguments, lineSpaces, startIndex, sequencePattern, context, reverse = false) {
|
|
107
|
+
let spaceIndex = startIndex;
|
|
108
|
+
const matchedSpaces = [];
|
|
109
|
+
const length = lineSpaces.length;
|
|
110
|
+
|
|
111
|
+
for (const chunk of sequencePattern) {
|
|
112
|
+
const { count, minCount, maxCount, conditions } = chunk;
|
|
113
|
+
|
|
114
|
+
let min, max;
|
|
115
|
+
if (count !== undefined) {
|
|
116
|
+
min = max = count;
|
|
117
|
+
} else if (minCount !== undefined || maxCount !== undefined) {
|
|
118
|
+
min = minCount || 0;
|
|
119
|
+
max = maxCount || Infinity;
|
|
120
|
+
} else {
|
|
121
|
+
min = max = 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let matchedCount = 0;
|
|
125
|
+
const chunkMatches = [];
|
|
126
|
+
|
|
127
|
+
// Greedy: try to match as many as possible up to max
|
|
128
|
+
while (matchedCount < max && spaceIndex < length) {
|
|
129
|
+
// Access space directly or in reverse without creating new array
|
|
130
|
+
const space = reverse
|
|
131
|
+
? lineSpaces[length - 1 - spaceIndex]
|
|
132
|
+
: lineSpaces[spaceIndex];
|
|
133
|
+
|
|
134
|
+
// Pass all previously matched spaces in this chunk
|
|
135
|
+
if (checkSpaceConditions(bgioArguments, space, conditions, chunkMatches, context)) {
|
|
136
|
+
chunkMatches.push(space);
|
|
137
|
+
matchedCount++;
|
|
138
|
+
spaceIndex++;
|
|
139
|
+
} else {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check if we met the minimum requirement
|
|
145
|
+
if (matchedCount < min) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
matchedSpaces.push(...chunkMatches);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return matchedSpaces.length > 0 ? matchedSpaces : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function checkSpaceConditions(bgioArguments, space, conditions, chunkMatches = [], context) {
|
|
156
|
+
// Early exit if no conditions
|
|
157
|
+
if (!conditions || conditions.length === 0) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return checkConditions(
|
|
162
|
+
bgioArguments,
|
|
163
|
+
{ conditions },
|
|
164
|
+
{
|
|
165
|
+
target: space,
|
|
166
|
+
targets: [space, ...chunkMatches] // for ContainsSame, other group conditions
|
|
167
|
+
},
|
|
168
|
+
context
|
|
169
|
+
).conditionsAreMet
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export default function gridContainsSequence(bgioArguments, grid, sequencePattern, context) {
|
|
173
|
+
const cacheKey = getSequenceKey(sequencePattern, context);
|
|
174
|
+
let gridCache = sequenceCache.get(grid);
|
|
175
|
+
|
|
176
|
+
if (!gridCache) {
|
|
177
|
+
gridCache = new Map();
|
|
178
|
+
sequenceCache.set(grid, gridCache);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const gridStateKey = getGridStateKey(grid);
|
|
182
|
+
const cacheEntry = gridCache.get(cacheKey);
|
|
183
|
+
|
|
184
|
+
if (cacheEntry && cacheEntry.stateKey === gridStateKey) {
|
|
185
|
+
return cacheEntry.result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const matches = [];
|
|
189
|
+
|
|
190
|
+
const minSequenceLength = sequencePattern.reduce((sum, chunk) =>
|
|
191
|
+
sum + (chunk.minCount || chunk.count || 1), 0
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// For each direction, scan each row/column/diagonal once
|
|
195
|
+
for (const [dx, dy] of directions) {
|
|
196
|
+
const lines = getLineStartingPoints(grid, dx, dy);
|
|
197
|
+
|
|
198
|
+
for (const [startX, startY] of lines) {
|
|
199
|
+
const lineSpaces = getLineSpaces(grid, startX, startY, dx, dy);
|
|
200
|
+
|
|
201
|
+
if (lineSpaces.length < minSequenceLength) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// todo: this forward/backward logic seems jank. why split them up?
|
|
206
|
+
|
|
207
|
+
const forwardMatches = findSequencesInLine(bgioArguments, lineSpaces, sequencePattern, minSequenceLength, context);
|
|
208
|
+
matches.push(...forwardMatches);
|
|
209
|
+
|
|
210
|
+
// Only reverse if needed (avoid creating new arrays unnecessarily)
|
|
211
|
+
if (forwardMatches.length === 0 || sequencePattern.length > 1) {
|
|
212
|
+
const reverseMatches = findSequencesInLine(bgioArguments, lineSpaces, sequencePattern, minSequenceLength, context, true);
|
|
213
|
+
matches.push(...reverseMatches);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const result = { matches, conditionIsMet: !!matches.length };
|
|
219
|
+
|
|
220
|
+
gridCache.set(cacheKey, {
|
|
221
|
+
stateKey: gridStateKey,
|
|
222
|
+
result
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return result;
|
|
226
|
+
}
|