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.
Files changed (102) hide show
  1. package/board-game-engine.test.js +2 -41
  2. package/dist/board-game-engine.js +32556 -5074
  3. package/dist/board-game-engine.min.js +2 -1
  4. package/dist/board-game-engine.min.js.LICENSE.txt +14 -0
  5. package/package.json +6 -3
  6. package/patch-bgio.cjs +23 -0
  7. package/src/client/client.js +287 -0
  8. package/src/game-factory/bank/bank-slot.js +69 -0
  9. package/src/game-factory/bank/bank.js +114 -0
  10. package/src/game-factory/board.js +3 -0
  11. package/src/game-factory/condition/condition-factory.js +52 -0
  12. package/src/game-factory/condition/condition.js +39 -0
  13. package/src/game-factory/condition/contains-condition.js +21 -0
  14. package/src/game-factory/condition/contains-same-condition.js +27 -0
  15. package/src/game-factory/condition/evaluate-condition.js +18 -0
  16. package/src/game-factory/condition/every-condition.js +25 -0
  17. package/src/game-factory/condition/has-line-condition.js +14 -0
  18. package/src/game-factory/condition/in-line-condition.js +19 -0
  19. package/src/game-factory/condition/is-condition.js +23 -0
  20. package/src/game-factory/condition/is-full-condition.js +9 -0
  21. package/src/game-factory/condition/no-possible-moves-condition.js +14 -0
  22. package/src/game-factory/condition/not-condition.js +14 -0
  23. package/src/game-factory/condition/or-condition.js +15 -0
  24. package/src/game-factory/condition/position-condition.js +12 -0
  25. package/src/game-factory/condition/some-condition.js +24 -0
  26. package/src/game-factory/condition/would-condition.js +94 -0
  27. package/src/game-factory/entity.js +29 -0
  28. package/src/game-factory/expand-game-rules.js +276 -0
  29. package/src/game-factory/game-factory.js +239 -0
  30. package/src/game-factory/move/end-turn.js +7 -0
  31. package/src/game-factory/move/for-each.js +18 -0
  32. package/src/game-factory/move/index.js +7 -0
  33. package/src/game-factory/move/move-entity.js +16 -0
  34. package/src/game-factory/move/move-factory.js +89 -0
  35. package/src/game-factory/move/move.js +131 -0
  36. package/src/game-factory/move/pass-turn.js +10 -0
  37. package/src/game-factory/move/pass.js +7 -0
  38. package/src/game-factory/move/place-new.js +33 -0
  39. package/src/game-factory/move/remove-entity.js +7 -0
  40. package/src/game-factory/move/set-active-players.js +23 -0
  41. package/src/game-factory/move/set-state.js +13 -0
  42. package/src/game-factory/move/shuffle.js +7 -0
  43. package/src/game-factory/move/take-from.js +7 -0
  44. package/src/game-factory/space/space.js +30 -0
  45. package/src/game-factory/space-group/grid.js +43 -0
  46. package/src/game-factory/space-group/space-group.js +29 -0
  47. package/src/index.js +2 -0
  48. package/src/registry.js +17 -0
  49. package/src/utils/any-valid-moves.js +157 -0
  50. package/src/utils/check-conditions.js +28 -0
  51. package/src/utils/create-payload.js +16 -0
  52. package/src/utils/deserialize-bgio-arguments.js +8 -0
  53. package/src/utils/do-moves.js +18 -0
  54. package/src/utils/entity-matches.js +20 -0
  55. package/src/utils/find-met-condition.js +22 -0
  56. package/src/utils/get-current-moves.js +12 -0
  57. package/src/utils/get-scenario-results.js +23 -0
  58. package/src/utils/get-steps.js +28 -0
  59. package/src/utils/get.js +25 -0
  60. package/src/utils/grid-contains-sequence.js +226 -0
  61. package/src/utils/json-transformer.js +12 -0
  62. package/src/utils/prepare-payload.js +16 -0
  63. package/src/utils/resolve-entity.js +9 -0
  64. package/src/utils/resolve-expression.js +10 -0
  65. package/src/utils/resolve-properties.js +157 -0
  66. package/src/utils/simulate-move.js +25 -0
  67. package/webpack.config.js +4 -1
  68. package/src/action/action-factory.js +0 -13
  69. package/src/action/action.js +0 -34
  70. package/src/action/move-piece-action.js +0 -11
  71. package/src/action/select-piece-action.js +0 -23
  72. package/src/action/swap-action.js +0 -14
  73. package/src/board/board-factory.js +0 -12
  74. package/src/board/board-group.js +0 -9
  75. package/src/board/board.js +0 -11
  76. package/src/board/grid.js +0 -52
  77. package/src/board/stack.js +0 -16
  78. package/src/condition/action-type-matches-condition.js +0 -7
  79. package/src/condition/bingo-condition.js +0 -50
  80. package/src/condition/blackout-condition.js +0 -9
  81. package/src/condition/condition-factory.js +0 -31
  82. package/src/condition/condition.js +0 -9
  83. package/src/condition/contains-condition.js +0 -14
  84. package/src/condition/does-not-contain-condition.js +0 -15
  85. package/src/condition/is-valid-player-condition.js +0 -7
  86. package/src/condition/piece-matches-condition.js +0 -23
  87. package/src/condition/relative-move-condition.js +0 -16
  88. package/src/condition/some-condition.js +0 -7
  89. package/src/game/game.ts +0 -362
  90. package/src/index.ts +0 -1
  91. package/src/piece/piece-factory.js +0 -5
  92. package/src/piece/piece.ts +0 -25
  93. package/src/piece/pile.js +0 -70
  94. package/src/player/player.ts +0 -13
  95. package/src/registry.ts +0 -51
  96. package/src/round/round-factory.js +0 -7
  97. package/src/round/round.js +0 -41
  98. package/src/round/sequential-player-turn.js +0 -18
  99. package/src/space/space.ts +0 -22
  100. package/src/utils/find-value-path.js +0 -37
  101. package/src/utils/resolve-board.ts +0 -38
  102. 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
@@ -0,0 +1,2 @@
1
+ export { Client } from './client/client'
2
+ export { default as gameFactory } from './game-factory/game-factory'
@@ -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,8 @@
1
+ import { deserialize } from "wackson";
2
+ import { registry } from "../registry.js";
3
+ export default function deserializeBgioArguments (bgioArguments) {
4
+ return {
5
+ ...bgioArguments,
6
+ G: deserialize(JSON.stringify(bgioArguments.G), registry)
7
+ }
8
+ }
@@ -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
+ }
@@ -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
+ }