@slugbugblue/trax 0.13.0 → 0.14.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @slugbugblue/trax
2
2
 
3
+ ## 0.14.0 - 2022-12-05
4
+
5
+ - Added `puzzles` to the CLI:
6
+
7
+ - `trax puzzles ls`: see all the puzzles
8
+ - `trax puzzle new`: start a new puzzle
9
+
10
+ - Bot tests now include solving all tutorial and easy puzzles. As the bot
11
+ improves, we will add more puzzles to the tests.
12
+
3
13
  ## 0.13.0 - 2022-11-25
4
14
 
5
15
  - Add move suggestions to `analyst.js` for CLI and bot use
package/README.md CHANGED
@@ -154,7 +154,6 @@ publicly available puzzles at [the traxgame.com puzzles page][traxpuzzles].
154
154
  ## Roadmap
155
155
 
156
156
  - Improve the threats database / analysis engine.
157
- - Add a puzzle interface to the CLI.
158
157
  - Add more puzzles.
159
158
 
160
159
  ## Support
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slugbugblue/trax",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Trax game engine and friends",
5
5
  "keywords": [
6
6
  "trax",
package/src/analyst.js CHANGED
@@ -200,7 +200,8 @@ const threatCount = (from) => {
200
200
  const passiveAttack = (from) =>
201
201
  attackCount(from) > 1 || (attackCount(from) === 1 && threatCount(from) > 0)
202
202
 
203
- const underAttack = (from) => attackCount(from) + threatCount(from) > 0
203
+ // Maybe we don't need this one
204
+ // const underAttack = (from) => attackCount(from) + threatCount(from) > 0
204
205
 
205
206
  // Analysis class
206
207
  export const Analysis = class {
@@ -295,7 +296,7 @@ export const Analysis = class {
295
296
  }
296
297
 
297
298
  const passive = game.color !== color
298
- const calc = !passive || passiveAttack(inactive) || underAttack(active)
299
+ const calc = !passive || passiveAttack(inactive)
299
300
 
300
301
  // Calculate the effects of the passive player first
301
302
  for (const level of Object.keys(inactive).map((i) => int(i))) {
package/src/cli.js CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  import { listCmd } from './cmds/list.js'
40
40
  import { newCmd } from './cmds/new.js'
41
41
  import { playCmd, tryCmd } from './cmds/play-try.js'
42
+ import { puzzlesCmd } from './cmds/puzzles.js'
42
43
  import { selectCmd } from './cmds/select.js'
43
44
  import { suggestCmd } from './cmds/suggest.js'
44
45
  import { undoCmd } from './cmds/undo.js'
@@ -87,6 +88,7 @@ const plugins = [
87
88
  listCmd,
88
89
  newCmd,
89
90
  playCmd,
91
+ puzzlesCmd,
90
92
  quitCmd,
91
93
  selectCmd,
92
94
  suggestCmd,
@@ -190,8 +192,8 @@ const color = (text, def) => {
190
192
  }
191
193
 
192
194
  // Internal representations of files
193
- const CONFIG = { id: '' }
194
- const DATA = { games: {} }
195
+ const CONFIG = { id: '', puzzleLevel: 0 }
196
+ const DATA = { games: {}, puzzles: {} }
195
197
 
196
198
  // CLI context object for plugins
197
199
  const CLI = {
@@ -215,6 +217,10 @@ const CLI = {
215
217
  get ID() {
216
218
  return CONFIG.id
217
219
  },
220
+ // The current puzzle level
221
+ get puzzleLevel() {
222
+ return CONFIG.puzzleLevel
223
+ },
218
224
 
219
225
  // Provide access to the current Trax object
220
226
  TRAX: new Trax(),
@@ -231,6 +237,7 @@ const CLI = {
231
237
  variable: 123,
232
238
  optional: 110,
233
239
  short: 153,
240
+ success: 35,
234
241
  white: 250,
235
242
  },
236
243
 
@@ -298,11 +305,57 @@ const CLI = {
298
305
  return id
299
306
  },
300
307
 
308
+ newPuzzle(puzzle) {
309
+ // Starts a new puzzle
310
+ const id = newId(DATA)
311
+ const trax = new Trax(puzzle.game, puzzle.notation, 'cli')
312
+ const players = ['challenger', 'puzzlebot']
313
+ DATA.games[id] = {
314
+ id,
315
+ puzzle: puzzle.id,
316
+ rules: trax.rules,
317
+ name: trax.name,
318
+ turn: trax.turn,
319
+ max: puzzle.max,
320
+ players: trax.turn === 1 ? players : players.reverse(),
321
+ moves: trax.moves.join(' '),
322
+ }
323
+
324
+ if (DATA.puzzles[puzzle.id]) {
325
+ DATA.puzzles[puzzle.id].attempts += 1
326
+ } else {
327
+ DATA.puzzles[puzzle.id] = { attempts: 1, solved: 0 }
328
+ }
329
+
330
+ CONFIG.id = id
331
+ CONFIG.puzzleLevel = puzzle.level
332
+ CLI.TRAX = trax
333
+
334
+ saveConfig()
335
+ saveData()
336
+
337
+ return id
338
+ },
339
+
301
340
  out(text) {
302
341
  // Prints to stdout
303
342
  console.info(text + tty.color(''))
304
343
  },
305
344
 
345
+ plural(n, noun, nouns) {
346
+ return String(n) + ' ' + (Math.abs(n) === 1 ? noun : nouns || noun + 's')
347
+ },
348
+
349
+ puzzle(id) {
350
+ return DATA.puzzles?.[id] || { attempts: 0, solved: 0 }
351
+ },
352
+
353
+ puzzleSolved(id) {
354
+ DATA.puzzles = DATA.puzzles || {}
355
+ DATA.puzzles[id] = DATA.puzzles[id] || { attempts: 1, solved: 0 }
356
+ DATA.puzzles[id].solved += 1
357
+ },
358
+
306
359
  resolveCommand(word) {
307
360
  // Determine if an entered word is a valid command.
308
361
  // Returns the command if a single match is found, an array if multiple
@@ -22,6 +22,7 @@ import envPaths from 'env-paths'
22
22
  import makeDir from 'make-dir'
23
23
 
24
24
  import { Trax } from '@slugbugblue/trax'
25
+ import { puzzles, sources } from '@slugbugblue/trax/puzzles'
25
26
 
26
27
  const paths = envPaths('trax', { suffix: '' })
27
28
  const PATHS = {}
@@ -30,7 +31,12 @@ const PATHS = {}
30
31
  let cli = {}
31
32
 
32
33
  const fsOuch = (fsError) => {
33
- cli.out(fsError, cli.COLORS.fatal)
34
+ if (cli.out) {
35
+ cli.out(fsError, cli.COLORS.fatal)
36
+ } else {
37
+ console.log(fsError)
38
+ }
39
+
34
40
  throw fsError
35
41
  }
36
42
 
@@ -72,7 +78,12 @@ export const getFile = async (filetype, filename, variable, decode) => {
72
78
  }
73
79
  } catch (fsError) {
74
80
  if (fsError instanceof TypeError || fsError instanceof SyntaxError) {
75
- cli.error(`Unable to read ${filetype} file ${path}/${filename}`)
81
+ const error = `Unable to read ${filetype} file ${path}/${filename}`
82
+ if (cli.error) {
83
+ cli.error(error)
84
+ } else {
85
+ console.log(error)
86
+ }
76
87
  } else if (fsError.code !== 'ENOENT') {
77
88
  fsOuch(fsError)
78
89
  }
@@ -200,6 +211,25 @@ exportCmd.fn = (CLI, id, filename) => {
200
211
  content += ' vs '
201
212
  content += game.players?.[1] || 'black'
202
213
  content += '\n; @slugbugblue/trax cli.js v' + CLI.version + '\n'
214
+ if (game.puzzle) {
215
+ const puzzle = puzzles.find((p) => p.id === game.puzzle)
216
+ if (puzzle) {
217
+ const source = sources[puzzle.src]
218
+ content += '; Puzzle ' + game.puzzle
219
+ if (source) {
220
+ content += ' provided courtesy of ' + source.name
221
+ if (source.url) content += ' (' + source.url + ')'
222
+ }
223
+
224
+ if (puzzle.title) content += '\n; ' + puzzle.title
225
+ if (puzzle.desc) content += '\n; ' + puzzle.desc
226
+ content += '\n; Starting position ' + puzzle.notation + '\n; '
227
+ const players = [0, 'white', 'black']
228
+ content += players[puzzle.player] + ' to win by move '
229
+ content += String(puzzle.max) + '\n'
230
+ }
231
+ }
232
+
203
233
  content += trax.notation + '\n'
204
234
  if (filename) filename = filename.replace(/[^ a-z\d.~/]/g, '')
205
235
  if (filename) {
package/src/cmds/list.js CHANGED
@@ -40,9 +40,6 @@ export const listCmd = {
40
40
  ],
41
41
  }
42
42
 
43
- const plural = (n, noun, nouns) =>
44
- n + ' ' + (Math.abs(n) === 1 ? noun : nouns || noun + 's')
45
-
46
43
  const alphaSort = (a, b) => {
47
44
  a = String(a).toLowerCase()
48
45
  b = String(b).toLowerCase()
@@ -120,7 +117,7 @@ listCmd.fn = (CLI, ...filters) => {
120
117
  id: (game.id === CLI.GAME.id ? '* #' : '#') + String(game.id),
121
118
  name: game.name?.length || 0,
122
119
  rules: game.rules,
123
- moves: plural(moves, 'move'),
120
+ moves: CLI.plural(moves, 'move'),
124
121
  smoves: game.over ? '9999' : String(moves).padStart(9, '0'),
125
122
  sturn: String(game.turn),
126
123
  p1: game.players?.[0] || 'white',
package/src/cmds/new.js CHANGED
@@ -42,7 +42,7 @@ const pairs = [
42
42
  ['Donald', 'Daisy'],
43
43
  ['Fred', 'Barney'],
44
44
  ['Fred', 'George'],
45
- ['Frodo', 'Sam'],
45
+ ['Frodo', 'Samwise'],
46
46
  ['Han', 'Chewie'],
47
47
  ['Harry', 'Ron'],
48
48
  ['Holmes', 'Watson'],
@@ -15,6 +15,8 @@
15
15
 
16
16
  // CLI play/try commands
17
17
 
18
+ import { suggest } from '@slugbugblue/trax/analyst'
19
+
18
20
  const moveRx = /^\d+\.?$/
19
21
  const notationRx = /^[@a-z]+\d+[bps/\\+]$/i
20
22
 
@@ -46,6 +48,40 @@ export const tryCmd = {
46
48
  ],
47
49
  }
48
50
 
51
+ const bot = (CLI) => {
52
+ const game = CLI.GAME
53
+ const trax = CLI.TRAX
54
+ const players = game.players || ['a', 'b']
55
+ const name = players[trax.turn - 1]
56
+ if (trax.over && game.puzzle) {
57
+ if (trax.move <= game.max && name !== 'puzzlebot') {
58
+ CLI.puzzleSolved(game.puzzle)
59
+ return 'win'
60
+ }
61
+
62
+ return 'lose'
63
+ }
64
+
65
+ if (!trax.over && /bot\b/i.test(name)) {
66
+ if (game.puzzle && trax.move >= game.max) {
67
+ trax.play('puzzled')
68
+ return 'lose'
69
+ }
70
+
71
+ CLI.do('view')
72
+ CLI.out(CLI.bubble(trax.color + 'd', `${name} thinking...`))
73
+ const move = suggest(trax)?.pick?.move
74
+ if (move) {
75
+ CLI.out(CLI.bubble(trax.color + 'd', `${name} chooses ${move}`))
76
+ trax.play(move)
77
+ }
78
+
79
+ if (game.puzzle) return ' and win by move ' + String(game.max)
80
+ }
81
+
82
+ return ''
83
+ }
84
+
49
85
  playCmd.fn = (CLI, ...moves) => {
50
86
  if (moves.length === 0 || !moves[0]) {
51
87
  CLI.error('You must provide a move.')
@@ -56,24 +92,27 @@ playCmd.fn = (CLI, ...moves) => {
56
92
  CLI.do('new')
57
93
  }
58
94
 
59
- if (CLI.TRAX.over) {
95
+ const game = CLI.GAME
96
+ const trax = CLI.TRAX
97
+
98
+ if (trax.over) {
60
99
  return CLI.error('The game is over.')
61
100
  }
62
101
 
63
- const start = CLI.TRAX.move
102
+ const start = trax.move
64
103
  let checkMove
65
104
 
66
105
  for (let move of moves) {
67
106
  if (notationRx.test(move)) {
68
- const moveNumber = CLI.TRAX.move
107
+ const moveNumber = trax.move
69
108
  move = CLI.fixNotation(move)
70
109
  if (checkMove) {
71
- CLI.TRAX.play(checkMove, move)
110
+ trax.play(checkMove, move)
72
111
  } else {
73
- CLI.TRAX.play(move)
112
+ trax.play(move)
74
113
  }
75
114
 
76
- if (CLI.TRAX.move === moveNumber) {
115
+ if (trax.move === moveNumber) {
77
116
  CLI.error(
78
117
  'Move "' +
79
118
  (checkMove ? checkMove + '. ' : '') +
@@ -88,9 +127,17 @@ playCmd.fn = (CLI, ...moves) => {
88
127
  }
89
128
  }
90
129
 
91
- if (CLI.TRAX.move !== start) {
130
+ if (trax.move !== start) {
131
+ const result = bot(CLI)
92
132
  CLI.updateGameData()
93
133
  CLI.do('view')
134
+ if (result === 'lose') {
135
+ CLI.error('Failed to complete puzzle ' + game.puzzle)
136
+ } else if (result === 'win') {
137
+ CLI.out(CLI.color.success('You completed puzzle ' + game.puzzle))
138
+ } else if (result) {
139
+ CLI.out(result)
140
+ }
94
141
  }
95
142
  }
96
143
 
@@ -0,0 +1,152 @@
1
+ /** @file Puzzles CLI
2
+ * @copyright 2022
3
+ * @author Chad Transtrum <chad@transtrum.net>
4
+ * @license Apache-2.0
5
+ */
6
+
7
+ import { Trax } from '@slugbugblue/trax'
8
+ import { levels, puzzles, sources } from '@slugbugblue/trax/puzzles'
9
+
10
+ let maxLevel = 0
11
+ const puzzleIds = []
12
+ for (const puzzle of puzzles) {
13
+ maxLevel = Math.max(maxLevel, puzzle.level)
14
+ puzzleIds.push(puzzle.id)
15
+ }
16
+
17
+ const validLevels = levels.slice(0, maxLevel + 1)
18
+ const completions = [...validLevels, ...puzzleIds].join('|')
19
+
20
+ export const puzzlesCmd = {
21
+ name: 'puzzles',
22
+ args: '["list"|"start" [<difficulty>|<puzzleID>]]',
23
+ comp: 'list|ls|new|show|start|view ' + completions,
24
+ desc: 'play puzzles',
25
+ help: [
26
+ 'Practice your skills with puzzles. Use "list" to see the puzzle',
27
+ 'categories, or "start" to play a puzzle.',
28
+ ],
29
+ }
30
+
31
+ const showPuzzle = (CLI, id) => {
32
+ const puzzle = puzzles.find((p) => p.id === id)
33
+ if (!puzzle) return CLI.error(`Puzzle ${id} not found`)
34
+ const trax = new Trax(puzzle.game, puzzle.notation, 'cli')
35
+ const players = ['white', 'black']
36
+ CLI.display(trax, players)
37
+ let out = CLI.color.short(puzzle.id)
38
+ out += CLI.color(' is a ' + levels[puzzle.level] + ' puzzle') + '\n'
39
+ if (puzzle.title) out += CLI.color(puzzle.title) + '\n'
40
+ if (puzzle.desc) out += CLI.color.help(puzzle.desc) + '\n'
41
+ out += CLI.color(players[puzzle.player - 1] + ' to win by move ' + puzzle.max)
42
+ const src = sources[puzzle.src]
43
+ if (src) {
44
+ let source = '\nPuzzle '
45
+ source += src.copyright
46
+ ? 'copyright ' + src.copyright + ' by'
47
+ : 'courtesy of'
48
+ source += ' ' + src.name
49
+ const url = src.url ? ' ' + CLI.color.white(src.url) : ''
50
+ out += CLI.color.black(source) + url
51
+ }
52
+
53
+ CLI.out(out)
54
+ }
55
+
56
+ const listPuzzles = (CLI) => {
57
+ let size = 1
58
+ const data = []
59
+ for (const name of validLevels) {
60
+ size = Math.max(size, name.length)
61
+ data.push({ name, count: 0, solved: 0, selected: false })
62
+ }
63
+
64
+ if (data[CLI.puzzleLevel]) data[CLI.puzzleLevel].selected = true
65
+
66
+ for (const puzzle of puzzles) {
67
+ data[puzzle.level].count += 1
68
+ if (CLI.puzzle(puzzle.id).solved > 0) data[puzzle.level].solved += 1
69
+ }
70
+
71
+ for (const category of data) {
72
+ let out = category.selected ? CLI.color('* ') : ' '
73
+ out += CLI.color.short(category.name.padEnd(size + 1))
74
+ if (category.solved === 0) {
75
+ out += CLI.color(CLI.plural(category.count, 'puzzle'))
76
+ } else if (category.solved === category.count) {
77
+ if (category.count > 3) out += CLI.color.success('all ')
78
+ out += CLI.color.success(CLI.plural(category.count, 'puzzle') + ' solved')
79
+ } else {
80
+ out += CLI.color.success(`${category.solved} solved`)
81
+ out += CLI.color(` of ${category.count} puzzles`)
82
+ }
83
+
84
+ CLI.out(out)
85
+ }
86
+ }
87
+
88
+ const listLevel = (CLI, levelName) => {
89
+ let size = 1
90
+ const level = levels.indexOf(levelName)
91
+ const data = []
92
+ for (const puzzle of puzzles) {
93
+ if (puzzle.level !== level) continue
94
+ size = Math.max(size, puzzle.id.length)
95
+ data.push({
96
+ id: puzzle.id,
97
+ title: puzzle.title,
98
+ solved: CLI.puzzle(puzzle.id).solved > 0,
99
+ })
100
+ }
101
+
102
+ for (const puzzle of data) {
103
+ let out = CLI.color.success(puzzle.solved ? '✓' : ' ')
104
+ out += CLI.color.short(' ' + puzzle.id)
105
+ if (puzzle.title) out += CLI.color(' ' + puzzle.title)
106
+ CLI.out(out)
107
+ }
108
+ }
109
+
110
+ const startPuzzle = (CLI, puzzle) => {
111
+ const id = CLI.newPuzzle(puzzle)
112
+ CLI.out(CLI.color(`Started puzzle ${puzzle.id} as game #${id}`))
113
+ showPuzzle(CLI, puzzle.id)
114
+ }
115
+
116
+ const startNextPuzzle = (CLI, level) => {
117
+ if (level === undefined) level = CLI.puzzleLevel
118
+ const unsolved = puzzles.filter((p) => !CLI.puzzle(p.id).solved)
119
+ if (unsolved.length === 0) {
120
+ CLI.out(CLI.success('You have already completed all the puzzles.'))
121
+ return CLI.error('Specify a specific puzzle to try one again.')
122
+ }
123
+
124
+ const next = unsolved.find((p) => p.level >= level) || unsolved[0]
125
+ startPuzzle(CLI, next)
126
+ }
127
+
128
+ puzzlesCmd.fn = (CLI, action, id) => {
129
+ action = action?.toLowerCase() ?? 'list'
130
+ if ('start'.startsWith(action) || 'new'.startsWith(action)) {
131
+ if (!id) return startNextPuzzle(CLI)
132
+
133
+ const puzzle = puzzles.find((p) => p.id === id)
134
+ if (puzzle) {
135
+ return startPuzzle(CLI, puzzle)
136
+ }
137
+
138
+ if (validLevels.includes(id)) {
139
+ return startNextPuzzle(CLI, levels.indexOf(id))
140
+ }
141
+
142
+ return CLI.error('Could not start puzzle ' + id)
143
+ }
144
+
145
+ if (action) {
146
+ if (validLevels.includes(id)) return listLevel(CLI, id)
147
+ if (puzzleIds.includes(id)) return showPuzzle(CLI, id)
148
+ if (id) return CLI.error('Could not find puzzle ' + id)
149
+ }
150
+
151
+ listPuzzles(CLI)
152
+ }
@@ -36,7 +36,7 @@ suggestCmd.fn = (CLI, debug) => {
36
36
 
37
37
  CLI.out(
38
38
  CLI.bubble(
39
- game.turn === '1' ? 'wh' : 'bh',
39
+ game.turn === 1 ? 'w' : 'b',
40
40
  String(game.move + 1) + '. ' + suggestion.pick.move,
41
41
  ),
42
42
  )
package/src/cmds/view.js CHANGED
@@ -49,5 +49,5 @@ viewCmd.fn = (CLI, move) => {
49
49
  CLI.color(' vs ') +
50
50
  CLI.bubble('b' + (trax.turn === 2 ? 'h' : ''), players[1]),
51
51
  )
52
- CLI.display(trax, players, false, show)
52
+ CLI.display(trax, players, undefined, show)
53
53
  }
package/src/puzzles.js CHANGED
@@ -4,10 +4,20 @@
4
4
  * @license Apache-2.0
5
5
  */
6
6
 
7
+ /** @type string[] */
8
+ export const levels = [
9
+ 'tutorial',
10
+ 'beginner',
11
+ 'tough',
12
+ 'advanced',
13
+ 'expert',
14
+ 'master',
15
+ ]
16
+
7
17
  /** @type {{[key: string]: PuzzleSource}} */
8
18
  export const sources = {
9
19
  sbb: {
10
- name: 'puzzlebot',
20
+ name: "slugbugblue's puzzlebot",
11
21
  },
12
22
  traxgame: {
13
23
  name: 'Donald G. Bailey',
@@ -25,8 +35,8 @@ export const puzzles = [
25
35
  icon: 'fd',
26
36
  player: 1,
27
37
  max: 3,
28
- level: 1,
29
- title: 'Tutorial: Loop attack',
38
+ level: 0,
39
+ title: 'Loop attack',
30
40
  desc: "You're playing white, so when you form a white loop, you win the game.",
31
41
  hints: ['A0/', 'B0\\'],
32
42
  },
@@ -38,8 +48,8 @@ export const puzzles = [
38
48
  icon: 'def',
39
49
  player: 2,
40
50
  max: 4,
41
- level: 1,
42
- title: 'Tutorial: Wide loop attack',
51
+ level: 0,
52
+ title: 'Wide loop attack',
43
53
  desc: "Your loop doesn't have to be a perfect circle, and it can be oriented in any direction.",
44
54
  hint: 'B2+',
45
55
  },
@@ -51,10 +61,10 @@ export const puzzles = [
51
61
  icon: 'ca:0e:0d',
52
62
  player: 1,
53
63
  max: 5,
54
- level: 1,
64
+ level: 0,
55
65
  title: 'Partially formed wide loop',
56
66
  desc: "It doesn't matter which direction the loops are facing.",
57
- hint: 'A2+',
67
+ hints: ['A2+', 'A3\\'],
58
68
  },
59
69
  {
60
70
  id: 'tutorial.04',
@@ -64,7 +74,7 @@ export const puzzles = [
64
74
  icon: 'd:aeec',
65
75
  player: 2,
66
76
  max: 6,
67
- level: 1,
77
+ level: 0,
68
78
  title: 'Using a hollow',
69
79
  desc: 'You can use forced tiles with an inside corner to stretch your attack.',
70
80
  hint: 'C1+',
@@ -77,7 +87,7 @@ export const puzzles = [
77
87
  icon: 'ca:0ef:0ec:0d',
78
88
  player: 1,
79
89
  max: 7,
80
- level: 1,
90
+ level: 0,
81
91
  title: 'Diverted attack',
82
92
  desc: "Even though black is attacking into this area, it doesn't stop you from completing your loop.",
83
93
  hint: 'A3+',
@@ -90,7 +100,7 @@ export const puzzles = [
90
100
  icon: 'd2f:aeeec',
91
101
  player: 2,
92
102
  max: 8,
93
- level: 1,
103
+ level: 0,
94
104
  title: 'Long cave',
95
105
  desc: 'Stretch the line with forced tiles.',
96
106
  hint: 'C1+',
@@ -103,7 +113,7 @@ export const puzzles = [
103
113
  icon: 'ae:0f:0a',
104
114
  player: 1,
105
115
  max: 7,
106
- level: 1,
116
+ level: 0,
107
117
  title: 'Introduction to the L',
108
118
  desc: 'Use an L to form two attacks at once.',
109
119
  hint: 'A3/',
@@ -116,7 +126,7 @@ export const puzzles = [
116
126
  icon: 'f:bae:1f',
117
127
  player: 2,
118
128
  max: 8,
119
- level: 1,
129
+ level: 0,
120
130
  title: 'Mirrored Ls',
121
131
  desc: "This mirrored position gives an L to both players, but since it's your turn, you have the initiative.",
122
132
  hint: 'A3/',
@@ -129,7 +139,7 @@ export const puzzles = [
129
139
  icon: 'a:efae',
130
140
  player: 2,
131
141
  max: 8,
132
- level: 1,
142
+ level: 0,
133
143
  title: 'Sideways L',
134
144
  desc: 'An L can be in any orientation.',
135
145
  hint: 'D1+',
@@ -142,7 +152,7 @@ export const puzzles = [
142
152
  icon: 'fae:1ef',
143
153
  player: 2,
144
154
  max: 8,
145
- level: 1,
155
+ level: 0,
146
156
  title: 'Ls come in many shapes',
147
157
  hint: 'A2/',
148
158
  },
@@ -150,13 +160,13 @@ export const puzzles = [
150
160
  id: 'tutorial.11',
151
161
  src: 'sbb',
152
162
  game: 'trax',
153
- notation: '@0+ @1\\ @1\\ D1+ D2\\',
163
+ notation: '@0+ B1/ C1/ @1+ A2/',
154
164
  icon: 'faee:2f',
155
165
  player: 2,
156
166
  max: 8,
157
- level: 1,
167
+ level: 0,
158
168
  title: 'Another L',
159
- hint: 'A2/',
169
+ hint: 'D2\\',
160
170
  },
161
171
  {
162
172
  id: 'tutorial.12',
@@ -166,7 +176,7 @@ export const puzzles = [
166
176
  icon: 'ae:fd:0ae',
167
177
  player: 2,
168
178
  max: 8,
169
- level: 1,
179
+ level: 0,
170
180
  title: 'Fat L',
171
181
  desc: "Even though this looks a bit odd, it's just another L.",
172
182
  hint: 'C1/',
@@ -179,7 +189,7 @@ export const puzzles = [
179
189
  icon: 'bbae:2f',
180
190
  player: 2,
181
191
  max: 8,
182
- level: 1,
192
+ level: 0,
183
193
  title: 'Connectable pair L',
184
194
  desc: 'This L uses a connectable pair instead of two corners, but it works the same as the other Ls.',
185
195
  hints: ['A2\\', 'B2/'],
@@ -188,14 +198,14 @@ export const puzzles = [
188
198
  id: 'tutorial.14',
189
199
  src: 'sbb',
190
200
  game: 'trax',
191
- notation: '@0+ @1\\ @1\\ C2+ C3+',
201
+ notation: '@0+ @1/ @1/ C0+ C0+',
192
202
  icon: 'fae:1e:1e',
193
203
  player: 2,
194
204
  max: 8,
195
- level: 1,
205
+ level: 0,
196
206
  title: 'Another connectable pair L',
197
207
  desc: 'There is a lot of variation in how Ls look, but being able to recognize them and activate them is key.',
198
- hint: 'A2/',
208
+ hint: 'A2\\',
199
209
  },
200
210
  {
201
211
  id: 'tutorial.15',
@@ -205,7 +215,7 @@ export const puzzles = [
205
215
  icon: 'e:e:d:ae',
206
216
  player: 2,
207
217
  max: 8,
208
- level: 1,
218
+ level: 0,
209
219
  title: 'Simple connectable pair L',
210
220
  desc: 'Starting to get the hang of this?',
211
221
  hints: ['B1\\', 'B2/'],
@@ -218,7 +228,7 @@ export const puzzles = [
218
228
  icon: 'a:e:eec',
219
229
  player: 2,
220
230
  max: 8,
221
- level: 1,
231
+ level: 0,
222
232
  title: 'Strange L',
223
233
  desc: 'Just another connectable pair L.',
224
234
  hint: 'B1/',
package/src/threats.js CHANGED
@@ -5,8 +5,8 @@
5
5
  */
6
6
 
7
7
  const REGEX = {
8
- a: '[am]', // White that pairs with the next white
9
- c: '[cp]', // White that forms a connectable pair with next white
8
+ a: '[am]', // White that pairs with the next white (m=hollow)
9
+ c: '[cp]', // White that forms a connectable pair with next white (p=hollow)
10
10
  l: '[lmp]', // White in a hollow
11
11
  w: '[aclmpw]', // Any white
12
12
  '!': '(?:b[acw])*?', // Ripple black/white
@@ -15,7 +15,7 @@ const REGEX = {
15
15
  '?': 'b?', // Zero or one black piece
16
16
  }
17
17
 
18
- class CustomInspectableThreat {
18
+ class Threat {
19
19
  #depth = 0
20
20
  #pattern = ''
21
21
  #rx = null
@@ -65,7 +65,7 @@ class CustomInspectableThreat {
65
65
  }
66
66
  }
67
67
 
68
- class CustomInspectableThreatsDB {
68
+ class ThreatsDB {
69
69
  #db = {}
70
70
  #depth = 0
71
71
 
@@ -77,7 +77,8 @@ class CustomInspectableThreatsDB {
77
77
 
78
78
  add(depth, pattern, reforms = false) {
79
79
  this.#depth = Math.max(depth, this.#depth)
80
- this.#db[pattern] = new CustomInspectableThreat(depth, pattern, reforms)
80
+ if (pattern in this.#db) throw new Error('Duplicate pattern:' + pattern)
81
+ this.#db[pattern] = new Threat(depth, pattern, reforms)
81
82
  return this.#db[pattern]
82
83
  }
83
84
 
@@ -110,8 +111,8 @@ class CustomInspectableThreatsDB {
110
111
  }
111
112
  }
112
113
 
113
- const pre = true
114
- const post = true
114
+ const pre = true // This pattern can appear first in an L threat
115
+ const post = true // This pattern can come second in an L threat
115
116
  const corners = {
116
117
  arw: { pre, post }, // Simple corner
117
118
  abrw: { post }, // Wide-first corner
@@ -122,7 +123,13 @@ const corners = {
122
123
  cbw: {}, // Wide connectable pair
123
124
  }
124
125
 
125
- export const threats = new CustomInspectableThreatsDB(Object.keys(corners))
126
+ export const threats = new ThreatsDB(Object.keys(corners))
127
+
128
+ // Attacks
129
+ const attacks = [
130
+ 'a?w', // Simple attack and wide attack
131
+ ]
132
+ attacks.map((a) => threats.add(1, a))
126
133
 
127
134
  // Re-forming attacks
128
135
  const reformAttacks = [
@@ -139,12 +146,6 @@ const reformThreats = [
139
146
  ]
140
147
  reformThreats.map((rt) => threats.add(2, rt))
141
148
 
142
- // Attacks
143
- const attacks = [
144
- 'a?w', // Simple attack and wide attack
145
- ]
146
- attacks.map((a) => threats.add(1, a))
147
-
148
149
  // Create multi-stage threats
149
150
  for (const one of Object.keys(corners)) {
150
151
  for (const two of Object.keys(corners)) {
@@ -152,9 +153,13 @@ for (const one of Object.keys(corners)) {
152
153
  const [before, after] = [corners[one].post, corners[two].pre]
153
154
  if (before || after) {
154
155
  const start = one.replace('w', 'l')
155
- threats.add(2, start + (before ? '?¡' : '¡') + two)
156
+ const pre = before ? '?' : ''
157
+ const post = after ? '?' : ''
158
+ threats.add(2, start + pre + '¡' + two) // Make an L
159
+ threats.add(3, start + pre + 'rb¡' + two) // Make a three-stage threat
156
160
  const end = two.replace('a', 'm').replace('c', 'p')
157
- threats.add(2, one + (after ? '?!' : '!') + end)
161
+ threats.add(2, one + post + '!' + end) // L
162
+ threats.add(3, one + post + '!br' + end) // Three-stage
158
163
  }
159
164
 
160
165
  // Edge threats
package/src/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '0.13.0'
2
+ export const version = '0.14.0'