@logic-pad/core 0.26.3 → 0.27.0

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.
@@ -2135,7 +2135,6 @@ declare global {
2135
2135
  withColor(color: Color): this;
2136
2136
  withCount(count: number): this;
2137
2137
  withComparison(comparison: Comparison): this;
2138
- private static countAllSymbolsOfPosition;
2139
2138
  }
2140
2139
  export declare class UniqueShapeRule extends RegionShapeRule {
2141
2140
  readonly title = 'Unique Shape Areas';
@@ -2538,7 +2537,7 @@ declare global {
2538
2537
  }
2539
2538
  export declare class SymbolsPerRegionBTModule extends BTModule {
2540
2539
  instr: SymbolsPerRegionRule;
2541
- private symbolCount;
2540
+ private symbolMap;
2542
2541
  constructor(
2543
2542
  instr: SymbolsPerRegionRule,
2544
2543
  width: number,
@@ -18,4 +18,5 @@ export interface BenchmarkEntry {
18
18
  solveCorrect: boolean;
19
19
  }
20
20
  export declare function parseLink(link: string): Promise<Puzzle>;
21
+ export declare function shuffleArray<T>(array: T[]): T[];
21
22
  export {};
@@ -32,3 +32,15 @@ export async function parseLink(link) {
32
32
  }
33
33
  return Serializer.parsePuzzle(await bunCompressor.decompress(data));
34
34
  }
35
+ export function shuffleArray(array) {
36
+ let currentIndex = array.length;
37
+ while (currentIndex !== 0) {
38
+ const randomIndex = Math.floor(Math.random() * currentIndex);
39
+ currentIndex--;
40
+ [array[currentIndex], array[randomIndex]] = [
41
+ array[randomIndex],
42
+ array[currentIndex],
43
+ ];
44
+ }
45
+ return array;
46
+ }
@@ -1,11 +1,18 @@
1
1
  import { parseArgs } from 'util';
2
2
  import { allSolvers } from '../src/data/solver/allSolvers.js';
3
- import { parseLink } from './helper.js';
3
+ import { parseLink, shuffleArray, } from './helper.js';
4
4
  import PQueue from 'p-queue';
5
- const allPuzzlesPath = 'benchmark/data/all_puzzles_logic_pad.json';
6
5
  const { values, positionals } = parseArgs({
7
6
  args: Bun.argv,
8
7
  options: {
8
+ name: {
9
+ type: 'string',
10
+ short: 'n',
11
+ },
12
+ file: {
13
+ type: 'string',
14
+ short: 'f',
15
+ },
9
16
  maxTime: {
10
17
  type: 'string',
11
18
  default: '10',
@@ -13,13 +20,13 @@ const { values, positionals } = parseArgs({
13
20
  },
14
21
  maxCount: {
15
22
  type: 'string',
16
- default: '99999',
23
+ default: '200',
17
24
  short: 'c',
18
25
  },
19
26
  concurrency: {
20
27
  type: 'string',
21
28
  default: '4',
22
- short: 'n',
29
+ short: 'd',
23
30
  },
24
31
  help: {
25
32
  type: 'boolean',
@@ -30,14 +37,17 @@ const { values, positionals } = parseArgs({
30
37
  allowPositionals: true,
31
38
  });
32
39
  positionals.splice(0, 2); // Remove "bun" and script name
33
- if (values.help || positionals.length !== 1) {
40
+ if (values.help || positionals.length !== 1 || !values.file) {
34
41
  console.log(`
35
42
  Usage: bun bench:prepare <solver> [options]
36
43
 
37
44
  Options:
45
+ -f, --file <string> Path to the puzzle data file (required)
46
+
47
+ -n, --name <string> Name of the generated benchmark files (default: solver name)
38
48
  -t, --maxTime <number> Maximum seconds allowed for each solve (default: 10)
39
49
  -c, --maxCount <number> Maximum number of puzzles included (default: 100)
40
- -n, --concurrency <number> Number of solves to run concurrently (default: 4)
50
+ -d, --concurrency <number> Number of solves to run concurrently (default: 4)
41
51
  -h, --help Show this help message
42
52
 
43
53
  Solver:
@@ -49,31 +59,36 @@ ${[...allSolvers.keys()].map(s => ` - ${s}`).join('\n')}
49
59
  const maxTime = parseFloat(values.maxTime) * 1000;
50
60
  const maxCount = parseInt(values.maxCount);
51
61
  const solverName = positionals[0];
62
+ const outputName = values.name ?? solverName;
63
+ const allPuzzlesPath = `benchmark/data/${values.file}`;
52
64
  const solver = allSolvers.get(solverName);
53
65
  if (!solver) {
54
66
  console.error(`Error: Solver "${solverName}" not found.`);
55
67
  process.exit(1);
56
68
  }
57
69
  const allPuzzles = (await Bun.file(allPuzzlesPath).json());
58
- allPuzzles.sort(() => Math.random() - 0.5);
70
+ shuffleArray(allPuzzles);
59
71
  const results = [];
60
72
  const pqueue = new PQueue({ concurrency: 4 });
61
- function printEntry(benchmarkEntry, entryId, solverId, pid) {
73
+ pqueue.on('completed', () => {
74
+ if (results.filter(r => r.result.solveCorrect && r.result.supported).length >=
75
+ maxCount) {
76
+ pqueue.clear();
77
+ }
78
+ });
79
+ function printEntry(benchmarkEntry, entryId, pid) {
80
+ const selectedPuzzles = results.filter(r => r.result.solveCorrect && r.result.supported);
62
81
  if (benchmarkEntry.supported) {
63
- console.log(`${solverId}\t| ${entryId} / ${allPuzzles.length}\t| ${pid}\t| ${Number.isNaN(benchmarkEntry.solveTime)
82
+ console.log(`${entryId} / ${allPuzzles.length} \t| ${selectedPuzzles.length} / ${maxCount} \t| ${pid}\t| ${Number.isNaN(benchmarkEntry.solveTime)
64
83
  ? 'timeout'
65
84
  : `${benchmarkEntry.solveTime.toFixed(0)}ms`} ${benchmarkEntry.solveCorrect ? '✓' : '✗'}`);
66
85
  }
67
86
  else {
68
- console.log(`${solverId}\t| ${entryId} / ${allPuzzles.length}\t| ${pid}\t| unsupported`);
87
+ console.log(`${entryId} / ${allPuzzles.length} \t| ${selectedPuzzles.length} / ${maxCount} \t| ${pid}\t| unsupported`);
69
88
  }
70
89
  }
71
90
  console.log('Available\t| Selected\t| PID\t| Result');
72
91
  for (const entry of allPuzzles) {
73
- if (results.filter(r => r.result.solveCorrect && r.result.supported).length >=
74
- maxCount) {
75
- break;
76
- }
77
92
  const puzzle = await parseLink(entry.puzzleLink);
78
93
  void pqueue.add(async () => {
79
94
  if (!solver.isGridSupported(puzzle.grid)) {
@@ -86,7 +101,7 @@ for (const entry of allPuzzles) {
86
101
  solveCorrect: false,
87
102
  },
88
103
  });
89
- printEntry(results[results.length - 1].result, results.length, solverName, entry.pid);
104
+ printEntry(results[results.length - 1].result, results.length, entry.pid);
90
105
  return;
91
106
  }
92
107
  const startTime = performance.now();
@@ -122,19 +137,18 @@ for (const entry of allPuzzles) {
122
137
  }
123
138
  clearTimeout(handle);
124
139
  results.push({ puzzle: entry, result: benchmarkEntry });
125
- printEntry(results[results.length - 1].result, results.length, solverName, entry.pid);
140
+ printEntry(results[results.length - 1].result, results.length, entry.pid);
126
141
  });
127
142
  }
128
143
  await pqueue.onIdle();
129
144
  const selectedPuzzles = results.filter(r => r.result.solveCorrect && r.result.supported);
130
145
  const benchmarkEntries = selectedPuzzles.map(r => r.result);
131
- await Bun.write(`benchmark/data/bench_${solverName}_puzzles.json`, JSON.stringify(selectedPuzzles.map(r => r.puzzle), null, 2));
132
- await Bun.write(`benchmark/data/bench_${solverName}_results.json`, JSON.stringify(benchmarkEntries, null, 2));
146
+ await Bun.write(`benchmark/data/${outputName}_bench_puzzles.json`, JSON.stringify(selectedPuzzles.map(r => r.puzzle), null, 2));
147
+ await Bun.write(`benchmark/data/${outputName}_bench_results.json`, JSON.stringify(benchmarkEntries, null, 2));
133
148
  console.log(`
134
149
  Benchmark preparation completed. Selected ${selectedPuzzles.length} puzzles.
135
150
 
136
151
  - Solver: ${solverName} Max Time: ${maxTime}ms Max Count: ${maxCount}
137
- - Average time: ${benchmarkEntries.filter(e => !Number.isNaN(e.solveTime)).reduce((a, b) => a + b.solveTime, 0) / benchmarkEntries.filter(e => !Number.isNaN(e.solveTime)).length}ms
138
- - Number of timeouts: ${benchmarkEntries.filter(e => Number.isNaN(e.solveTime)).length}
152
+ - Average time: ${selectedPuzzles.reduce((a, b) => a + b.result.solveTime, 0) / selectedPuzzles.length}ms
139
153
  - Solve correctness: ${benchmarkEntries.filter(e => e.solveCorrect).length} / ${benchmarkEntries.length}
140
154
  `);
@@ -1,10 +1,14 @@
1
1
  import { parseArgs } from 'util';
2
2
  import { allSolvers } from '../src/data/solver/allSolvers.js';
3
- import { parseLink } from './helper.js';
3
+ import { parseLink, shuffleArray, } from './helper.js';
4
4
  import PQueue from 'p-queue';
5
5
  const { values, positionals } = parseArgs({
6
6
  args: Bun.argv,
7
7
  options: {
8
+ name: {
9
+ type: 'string',
10
+ short: 'n',
11
+ },
8
12
  maxTime: {
9
13
  type: 'string',
10
14
  default: '10',
@@ -12,13 +16,13 @@ const { values, positionals } = parseArgs({
12
16
  },
13
17
  maxCount: {
14
18
  type: 'string',
15
- default: '99999',
19
+ default: '200',
16
20
  short: 'c',
17
21
  },
18
22
  concurrency: {
19
23
  type: 'string',
20
24
  default: '4',
21
- short: 'n',
25
+ short: 'd',
22
26
  },
23
27
  help: {
24
28
  type: 'boolean',
@@ -34,10 +38,11 @@ if (values.help || positionals.length === 0) {
34
38
  Usage: bun bench:run <solver> [options]
35
39
 
36
40
  Options:
37
- -t, --maxTime <number> Maximum seconds allowed for each solve (default: 10)
38
- -c, --maxCount <number> Maximum number of puzzles included (default: 100)
39
- -n, --concurrency <number> Number of solves to run concurrently (default: 4)
40
- -h, --help Show this help message
41
+ -n, --name <string> Name of the generated benchmark files (default: first solver name)
42
+ -t, --maxTime <number> Maximum seconds allowed for each solve (default: 10)
43
+ -c, --maxCount <number> Maximum number of puzzles included (default: 100)
44
+ -n, --concurrency <number> Number of solves to run concurrently (default: 4)
45
+ -h, --help Show this help message
41
46
 
42
47
  Solvers available for benchmarking:
43
48
  ${[...allSolvers.keys()].map(s => ` - ${s}`).join('\n')}
@@ -53,8 +58,9 @@ for (const name of positionals) {
53
58
  process.exit(1);
54
59
  }
55
60
  }
56
- const allPuzzles = (await Bun.file(`benchmark/data/bench_${positionals[0]}_puzzles.json`).json());
57
- allPuzzles.sort(() => Math.random() - 0.5);
61
+ const outputName = values.name ?? positionals[0];
62
+ const allPuzzles = (await Bun.file(`benchmark/data/${outputName}_bench_puzzles.json`).json());
63
+ shuffleArray(allPuzzles);
58
64
  allPuzzles.splice(maxCount);
59
65
  const benchmarkEntries = Object.fromEntries(positionals.map(name => [
60
66
  name,
@@ -139,9 +145,11 @@ const results = positionals.map(name => ({
139
145
  solve25: 0,
140
146
  solve50: 0,
141
147
  solve75: 0,
148
+ solve90: 0,
142
149
  solveSD: 0,
143
150
  unsupportedCount: 0,
144
151
  incorrectCount: 0,
152
+ timeoutCount: 0,
145
153
  }));
146
154
  for (let i = 0; i < benchmarkEntries[positionals[0]].length; i++) {
147
155
  let fastestSolveTime = Number.POSITIVE_INFINITY;
@@ -158,9 +166,12 @@ for (let i = 0; i < benchmarkEntries[positionals[0]].length; i++) {
158
166
  fastestSolverIndex = j;
159
167
  }
160
168
  }
161
- else {
169
+ else if (!Number.isNaN(entry.solveTime)) {
162
170
  results[j].incorrectCount++;
163
171
  }
172
+ else {
173
+ results[j].timeoutCount++;
174
+ }
164
175
  }
165
176
  if (fastestSolverIndex !== null) {
166
177
  results[fastestSolverIndex].fastestCount++;
@@ -184,6 +195,11 @@ for (let j = 0; j < positionals.length; j++) {
184
195
  times.length === 0
185
196
  ? Number.NaN
186
197
  : times[Math.floor((times.length - 1) * 0.75)];
198
+ // 90th percentile
199
+ results[j].solve90 =
200
+ times.length === 0
201
+ ? Number.NaN
202
+ : times[Math.floor((times.length - 1) * 0.9)];
187
203
  // Standard deviation
188
204
  const mean = times.reduce((sum, time) => sum + time, 0) / (times.length || 1);
189
205
  const variance = times.reduce((sum, time) => sum + (time - mean) ** 2, 0) /
@@ -199,8 +215,10 @@ ${result.solver}:
199
215
  P25: ${Number.isNaN(result.solve25) ? 'N/A' : `${result.solve25.toFixed(2)}ms`}
200
216
  P50: ${Number.isNaN(result.solve50) ? 'N/A' : `${result.solve50.toFixed(2)}ms`}
201
217
  P75: ${Number.isNaN(result.solve75) ? 'N/A' : `${result.solve75.toFixed(2)}ms`}
218
+ P90: ${Number.isNaN(result.solve90) ? 'N/A' : `${result.solve90.toFixed(2)}ms`}
202
219
  SD: ${Number.isNaN(result.solveSD) ? 'N/A' : `${result.solveSD.toFixed(2)}ms`}
203
220
  Unsupported Puzzles: ${result.unsupportedCount}
204
221
  Incorrect Solutions: ${result.incorrectCount}
222
+ Timeouts: ${result.timeoutCount}
205
223
  `);
206
224
  }
@@ -33,6 +33,5 @@ export default class SymbolsPerRegionRule extends Rule {
33
33
  withColor(color: Color): this;
34
34
  withCount(count: number): this;
35
35
  withComparison(comparison: Comparison): this;
36
- private static countAllSymbolsOfPosition;
37
36
  }
38
37
  export declare const instance: SymbolsPerRegionRule;
@@ -125,12 +125,24 @@ export default class SymbolsPerRegionRule extends Rule {
125
125
  break;
126
126
  const completed = [];
127
127
  const gray = [];
128
+ const map = array(grid.width, grid.height, () => false);
128
129
  let nbSymbolsIn = 0;
129
130
  grid.iterateArea({ x: seed.x, y: seed.y }, tile => tile.color === this.color, (_, x, y) => {
130
131
  completed.push({ x, y });
131
132
  visited[y][x] = true;
132
- nbSymbolsIn += SymbolsPerRegionRule.countAllSymbolsOfPosition(grid, x, y);
133
- });
133
+ }, map);
134
+ for (const [_, symbols] of grid.symbols) {
135
+ for (const symbol of symbols) {
136
+ if (!symbol.necessaryForCompletion)
137
+ continue;
138
+ if (map[Math.floor(symbol.y)][Math.floor(symbol.x)] ||
139
+ map[Math.floor(symbol.y)][Math.ceil(symbol.x)] ||
140
+ map[Math.ceil(symbol.y)][Math.floor(symbol.x)] ||
141
+ map[Math.ceil(symbol.y)][Math.ceil(symbol.x)]) {
142
+ nbSymbolsIn++;
143
+ }
144
+ }
145
+ }
134
146
  if (this.comparison !== Comparison.AtLeast && nbSymbolsIn > this.count) {
135
147
  return { state: State.Error, positions: completed };
136
148
  }
@@ -142,8 +154,20 @@ export default class SymbolsPerRegionRule extends Rule {
142
154
  else {
143
155
  grid.iterateArea({ x: seed.x, y: seed.y }, tile => tile.color === Color.Gray || tile.color === this.color, (_, x, y) => {
144
156
  gray.push({ x, y });
145
- nbSymbolsOut += SymbolsPerRegionRule.countAllSymbolsOfPosition(grid, x, y);
157
+ map[y][x] = true;
146
158
  });
159
+ for (const [_, symbols] of grid.symbols) {
160
+ for (const symbol of symbols) {
161
+ if (!symbol.necessaryForCompletion)
162
+ continue;
163
+ if (map[Math.floor(symbol.y)][Math.floor(symbol.x)] ||
164
+ map[Math.floor(symbol.y)][Math.ceil(symbol.x)] ||
165
+ map[Math.ceil(symbol.y)][Math.floor(symbol.x)] ||
166
+ map[Math.ceil(symbol.y)][Math.ceil(symbol.x)]) {
167
+ nbSymbolsOut++;
168
+ }
169
+ }
170
+ }
147
171
  }
148
172
  if (this.comparison !== Comparison.AtMost && nbSymbolsOut < this.count) {
149
173
  return { state: State.Error, positions: gray };
@@ -166,16 +190,5 @@ export default class SymbolsPerRegionRule extends Rule {
166
190
  withComparison(comparison) {
167
191
  return this.copyWith({ comparison });
168
192
  }
169
- static countAllSymbolsOfPosition(grid, x, y) {
170
- let count = 0;
171
- for (const symbolKind of grid.symbols.values()) {
172
- if (symbolKind.some(symbol => Math.floor(symbol.x) === x &&
173
- Math.floor(symbol.y) === y &&
174
- symbol.necessaryForCompletion)) {
175
- count++;
176
- }
177
- }
178
- return count;
179
- }
180
193
  }
181
194
  export const instance = new SymbolsPerRegionRule(Color.Dark, 1);
@@ -3,7 +3,7 @@ import Symbol from '../../../symbols/symbol.js';
3
3
  import BTModule, { BTGridData, CheckResult } from '../data.js';
4
4
  export default class SymbolsPerRegionBTModule extends BTModule {
5
5
  instr: SymbolsPerRegionRule;
6
- private symbolCount;
6
+ private symbolMap;
7
7
  constructor(instr: SymbolsPerRegionRule, width: number, height: number, allSymbols: Symbol[]);
8
8
  checkGlobal(grid: BTGridData): CheckResult | false;
9
9
  private visitArea;
@@ -1,16 +1,33 @@
1
+ import { array } from '../../../dataHelper.js';
1
2
  import { Comparison } from '../../../primitives.js';
2
3
  import BTModule, { BTTile, IntArray2D, colorToBTTile, } from '../data.js';
3
4
  export default class SymbolsPerRegionBTModule extends BTModule {
4
5
  instr;
5
- symbolCount;
6
+ symbolMap = [];
6
7
  constructor(instr, width, height, allSymbols) {
7
8
  super();
8
9
  this.instr = instr;
9
- this.symbolCount = IntArray2D.create(width, height);
10
+ this.symbolMap = array(width, height, () => []);
10
11
  for (const symbol of allSymbols) {
11
- const symbolX = Math.floor(symbol.x);
12
- const symbolY = Math.floor(symbol.y);
13
- this.symbolCount.set(symbolX, symbolY, this.symbolCount.get(symbolX, symbolY) + 1);
12
+ if (Math.floor(symbol.x) >= 0 && Math.floor(symbol.y) >= 0) {
13
+ this.symbolMap[Math.floor(symbol.y)][Math.floor(symbol.x)].push(symbol);
14
+ }
15
+ if (Math.ceil(symbol.x) !== Math.floor(symbol.x) &&
16
+ Math.ceil(symbol.x) < width &&
17
+ Math.floor(symbol.y) >= 0) {
18
+ this.symbolMap[Math.floor(symbol.y)][Math.ceil(symbol.x)].push(symbol);
19
+ }
20
+ if (Math.ceil(symbol.y) !== Math.floor(symbol.y) &&
21
+ Math.floor(symbol.x) >= 0 &&
22
+ Math.ceil(symbol.y) < height) {
23
+ this.symbolMap[Math.ceil(symbol.y)][Math.floor(symbol.x)].push(symbol);
24
+ }
25
+ if (Math.ceil(symbol.x) !== Math.floor(symbol.x) &&
26
+ Math.ceil(symbol.y) !== Math.floor(symbol.y) &&
27
+ Math.ceil(symbol.x) < width &&
28
+ Math.ceil(symbol.y) < height) {
29
+ this.symbolMap[Math.ceil(symbol.y)][Math.ceil(symbol.x)].push(symbol);
30
+ }
14
31
  }
15
32
  }
16
33
  checkGlobal(grid) {
@@ -35,13 +52,13 @@ export default class SymbolsPerRegionBTModule extends BTModule {
35
52
  visitArea(grid, tile, visited, pos, id) {
36
53
  const sameTileQueue = [pos];
37
54
  const usableTileQueue = [];
38
- let completed = 0;
39
- let possible = 0;
55
+ const completed = new Set();
56
+ const possible = new Set();
40
57
  visited.set(pos.x, pos.y, id);
41
58
  // Count same tile
42
59
  while (sameTileQueue.length > 0) {
43
60
  const curPos = sameTileQueue.pop();
44
- completed += this.symbolCount.get(curPos.x, curPos.y);
61
+ this.symbolMap[curPos.y][curPos.x].forEach(symbol => completed.add(symbol));
45
62
  for (const edge of grid.getEdges(curPos)) {
46
63
  if ((visited.get(edge.x, edge.y) & 0b01111111) === id)
47
64
  continue;
@@ -56,7 +73,7 @@ export default class SymbolsPerRegionBTModule extends BTModule {
56
73
  }
57
74
  }
58
75
  }
59
- if (completed > this.instr.count) {
76
+ if (completed.size > this.instr.count) {
60
77
  return this.instr.comparison === Comparison.AtLeast;
61
78
  }
62
79
  if (this.instr.comparison === Comparison.AtMost)
@@ -64,8 +81,11 @@ export default class SymbolsPerRegionBTModule extends BTModule {
64
81
  // Count usable tile
65
82
  while (usableTileQueue.length > 0) {
66
83
  const curPos = usableTileQueue.pop();
67
- possible += this.symbolCount.get(curPos.x, curPos.y);
68
- if (completed + possible >= this.instr.count)
84
+ this.symbolMap[curPos.y][curPos.x].forEach(symbol => {
85
+ if (!completed.has(symbol))
86
+ possible.add(symbol);
87
+ });
88
+ if (completed.size + possible.size >= this.instr.count)
69
89
  return true;
70
90
  for (const edge of grid.getEdges(curPos)) {
71
91
  if ((visited.get(edge.x, edge.y) & 0b01111111) === id)
@@ -77,6 +97,6 @@ export default class SymbolsPerRegionBTModule extends BTModule {
77
97
  }
78
98
  }
79
99
  }
80
- return completed + possible >= this.instr.count;
100
+ return completed.size + possible.size >= this.instr.count;
81
101
  }
82
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logic-pad/core",
3
- "version": "0.26.3",
3
+ "version": "0.27.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -48,6 +48,8 @@
48
48
  "typegen": "bun ./scripts/genTypes.ts",
49
49
  "bench:prepare": "bun ./benchmark/prepareBench.ts",
50
50
  "bench:run": "bun ./benchmark/runBench.ts",
51
+ "bench:prepare:lp": "bun bench:prepare universal -f all_puzzles_logic_pad.json -n lp",
52
+ "bench:run:lp": "bun bench:run universal universal_dev -n lp",
51
53
  "prepublishOnly": "bun run build"
52
54
  },
53
55
  "engines": {