@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 +10 -0
- package/README.md +0 -1
- package/package.json +1 -1
- package/src/analyst.js +3 -2
- package/src/cli.js +55 -2
- package/src/cmds/import-export.js +32 -2
- package/src/cmds/list.js +1 -4
- package/src/cmds/new.js +1 -1
- package/src/cmds/play-try.js +54 -7
- package/src/cmds/puzzles.js +152 -0
- package/src/cmds/suggest.js +1 -1
- package/src/cmds/view.js +1 -1
- package/src/puzzles.js +34 -24
- package/src/threats.js +21 -16
- package/src/version.js +1 -1
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
package/package.json
CHANGED
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
package/src/cmds/play-try.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
107
|
+
const moveNumber = trax.move
|
|
69
108
|
move = CLI.fixNotation(move)
|
|
70
109
|
if (checkMove) {
|
|
71
|
-
|
|
110
|
+
trax.play(checkMove, move)
|
|
72
111
|
} else {
|
|
73
|
-
|
|
112
|
+
trax.play(move)
|
|
74
113
|
}
|
|
75
114
|
|
|
76
|
-
if (
|
|
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 (
|
|
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
|
+
}
|
package/src/cmds/suggest.js
CHANGED
package/src/cmds/view.js
CHANGED
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:
|
|
29
|
-
title: '
|
|
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:
|
|
42
|
-
title: '
|
|
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:
|
|
64
|
+
level: 0,
|
|
55
65
|
title: 'Partially formed wide loop',
|
|
56
66
|
desc: "It doesn't matter which direction the loops are facing.",
|
|
57
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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+
|
|
163
|
+
notation: '@0+ B1/ C1/ @1+ A2/',
|
|
154
164
|
icon: 'faee:2f',
|
|
155
165
|
player: 2,
|
|
156
166
|
max: 8,
|
|
157
|
-
level:
|
|
167
|
+
level: 0,
|
|
158
168
|
title: 'Another L',
|
|
159
|
-
hint: '
|
|
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:
|
|
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:
|
|
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
|
|
201
|
+
notation: '@0+ @1/ @1/ C0+ C0+',
|
|
192
202
|
icon: 'fae:1e:1e',
|
|
193
203
|
player: 2,
|
|
194
204
|
max: 8,
|
|
195
|
-
level:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 +
|
|
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.
|
|
2
|
+
export const version = '0.14.0'
|