@johnmorrisdotca/houseki 0.1.1 → 0.2.1

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.
@@ -61,7 +61,7 @@ function solveWitness(start, witness) {
61
61
  export function createChallenge(options) {
62
62
  if (!options || typeof options !== 'object' || Array.isArray(options))
63
63
  throw new StoneChainsOptionsError('invalid-challenge', 'Challenge options must be an object');
64
- const allowed = ['id', 'width', 'height', 'colourCount', 'seed', 'board', 'queue', 'goal', 'witness'];
64
+ const allowed = ['id', 'width', 'height', 'colourCount', 'seed', 'board', 'queue', 'magneticQueue', 'nature', 'weather', 'goal', 'witness'];
65
65
  if (Object.keys(options).some(key => !allowed.includes(key)))
66
66
  throw new StoneChainsOptionsError('unknown-challenge-option', 'Challenge options contain unsupported fields');
67
67
  if (typeof options.id !== 'string' || !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(options.id))
@@ -75,6 +75,14 @@ export function createChallenge(options) {
75
75
  const seed = options.seed ?? `challenge:${options.id}`;
76
76
  if (typeof seed !== 'string' || seed.length < 1 || seed.length > 200)
77
77
  throw new StoneChainsOptionsError('invalid-seed', 'Seed must contain 1–200 characters');
78
+ if (options.nature !== undefined && typeof options.nature !== 'boolean')
79
+ throw new StoneChainsOptionsError('invalid-nature-option', 'nature must be a boolean');
80
+ if (options.weather !== undefined && options.weather !== 'frequent' && options.weather !== 'rare')
81
+ throw new StoneChainsOptionsError('invalid-weather-option', 'weather must be frequent or rare');
82
+ if (options.weather !== undefined && options.nature !== true)
83
+ throw new StoneChainsOptionsError('weather-requires-nature', 'Arashi weather requires nature:true');
84
+ if (options.magneticQueue !== undefined && options.nature !== true)
85
+ throw new StoneChainsOptionsError('magnetic-queue-requires-nature', 'Magnetic queue flags require nature:true');
78
86
  if (!Array.isArray(options.board) || options.board.length !== width * height)
79
87
  throw new StoneChainsOptionsError('invalid-challenge-board', 'Challenge board must contain width × height visible cells');
80
88
  if (!Array.isArray(options.queue) || options.queue.length < 2 || options.queue.length > 12)
@@ -84,10 +92,10 @@ export function createChallenge(options) {
84
92
  const board = options.board.map((gem, index) => {
85
93
  if (gem === null)
86
94
  return null;
87
- if (!gem || typeof gem !== 'object' || Array.isArray(gem) || Object.keys(gem).some(key => key !== 'id' && key !== 'colour') || !Number.isSafeInteger(gem.id) || gem.id < 1 || gem.id > 0x7fff_ffff || ids.has(gem.id) || !allowedColours.includes(gem.colour))
95
+ if (!gem || typeof gem !== 'object' || Array.isArray(gem) || Object.keys(gem).some(key => key !== 'id' && key !== 'colour' && key !== 'magnetic') || !Number.isSafeInteger(gem.id) || gem.id < 1 || gem.id > 0x7fff_ffff || ids.has(gem.id) || !allowedColours.includes(gem.colour) || gem.magnetic !== undefined && (gem.magnetic !== true || options.nature !== true))
88
96
  throw new StoneChainsOptionsError('invalid-challenge-gem', `Invalid challenge gem at cell ${index}`);
89
97
  ids.add(gem.id);
90
- return Object.freeze({ id: gem.id, colour: gem.colour });
98
+ return Object.freeze({ id: gem.id, colour: gem.colour, ...(gem.magnetic ? { magnetic: true } : {}) });
91
99
  });
92
100
  for (let x = 0; x < width; x++) {
93
101
  let gap = false;
@@ -103,6 +111,12 @@ export function createChallenge(options) {
103
111
  throw new StoneChainsOptionsError('invalid-challenge-pair', `Invalid queue pair ${index + 1}`);
104
112
  return Object.freeze([pair[0], pair[1]]);
105
113
  });
114
+ let magneticQueue;
115
+ if (options.magneticQueue !== undefined) {
116
+ if (!Array.isArray(options.magneticQueue) || options.magneticQueue.length !== queue.length || options.magneticQueue.some(pair => !Array.isArray(pair) || pair.length !== 2 || pair.some(flag => typeof flag !== 'boolean')))
117
+ throw new StoneChainsOptionsError('invalid-magnetic-queue', 'Magnetic queue must contain one pair of boolean flags per finite queue pair');
118
+ magneticQueue = Object.freeze(options.magneticQueue.map(pair => Object.freeze([pair[0], pair[1]])));
119
+ }
106
120
  const goal = validateChallengeGoal(options.goal, ids);
107
121
  if (!Array.isArray(options.witness) || options.witness.length < 1 || options.witness.length > queue.length)
108
122
  throw new StoneChainsOptionsError('invalid-witness', 'A challenge requires a complete witness of 1–12 pair placements');
@@ -111,7 +125,7 @@ export function createChallenge(options) {
111
125
  throw new StoneChainsOptionsError('invalid-witness-step', `Invalid witness placement ${index + 1}`);
112
126
  return Object.freeze({ pivotX: step.pivotX, orientation: step.orientation });
113
127
  });
114
- const settings = Object.freeze({ mode: 'challenge', width, height, colourCount, seed, challengeId: options.id, goal, initialBoard: Object.freeze(board), queue: Object.freeze(queue), witness: Object.freeze(witness) });
128
+ const settings = Object.freeze({ mode: 'challenge', width, height, colourCount, seed, challengeId: options.id, goal, initialBoard: Object.freeze(board), queue: Object.freeze(queue), ...(magneticQueue ? { magneticQueue } : {}), ...(options.nature ? { nature: true } : {}), ...(options.weather ? { weather: options.weather } : {}), witness: Object.freeze(witness) });
115
129
  const state = initialChallengeState(settings, settings.initialBoard, settings.queue);
116
130
  const solved = solveWitness(state, witness);
117
131
  if (solved.phase !== 'won' || stable(solved.settings) !== stable(settings))