@slugbugblue/trax 0.12.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 +25 -0
- package/README.md +45 -7
- package/docs/analyst.md +44 -2
- package/package.json +6 -7
- package/src/analyst.js +97 -12
- package/src/cli.js +114 -31
- package/src/cmds/analyze.js +2 -2
- package/src/cmds/import-export.js +33 -3
- package/src/cmds/list.js +2 -5
- package/src/cmds/new.js +6 -4
- package/src/cmds/play-try.js +54 -7
- package/src/cmds/puzzles.js +152 -0
- package/src/cmds/suggest.js +52 -0
- package/src/cmds/view.js +1 -1
- package/src/engine.js +167 -29
- package/src/point.js +54 -12
- package/src/puzzles.js +808 -0
- package/src/threats.js +39 -35
- package/src/tty.js +30 -15
- package/src/types.d.ts +105 -0
- package/src/version.js +1 -1
|
@@ -21,7 +21,8 @@ import process from 'node:process'
|
|
|
21
21
|
import envPaths from 'env-paths'
|
|
22
22
|
import makeDir from 'make-dir'
|
|
23
23
|
|
|
24
|
-
import { Trax } from '
|
|
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
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
// CLI list command
|
|
17
17
|
|
|
18
|
-
import { Trax } from '
|
|
18
|
+
import { Trax } from '@slugbugblue/trax'
|
|
19
19
|
|
|
20
20
|
export const listCmd = {
|
|
21
21
|
name: 'list',
|
|
@@ -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 ? 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', '
|
|
45
|
+
['Frodo', 'Samwise'],
|
|
46
46
|
['Han', 'Chewie'],
|
|
47
47
|
['Harry', 'Ron'],
|
|
48
48
|
['Holmes', 'Watson'],
|
|
@@ -58,13 +58,13 @@ const pairs = [
|
|
|
58
58
|
['Mario', 'Luigi'],
|
|
59
59
|
['Marlin', 'Dory'],
|
|
60
60
|
['Mary-Kate', 'Ashley'],
|
|
61
|
-
['
|
|
61
|
+
['Mickey', 'Minnie'],
|
|
62
62
|
['Miss Piggy', 'Kermit'],
|
|
63
63
|
['Mork', 'Mindy'],
|
|
64
64
|
['Obi-Wan', 'Anakin'],
|
|
65
65
|
['Pan', 'Hook'],
|
|
66
66
|
['Phineas', 'Ferb'],
|
|
67
|
-
['
|
|
67
|
+
['player 1', 'player 2'],
|
|
68
68
|
['p1', 'p2'],
|
|
69
69
|
['R2-D2', 'C-3PO'],
|
|
70
70
|
['Ren', 'Stimpy'],
|
|
@@ -89,7 +89,9 @@ const pairs = [
|
|
|
89
89
|
]
|
|
90
90
|
|
|
91
91
|
const pairUp = () => {
|
|
92
|
-
|
|
92
|
+
const duo = pairs[Math.floor(Math.random() * pairs.length)]
|
|
93
|
+
if (duo[0].toLowerCase() !== duo[0] && Math.random() < 0.5) duo.reverse()
|
|
94
|
+
return duo
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
newCmd.fn = (CLI, variant, ...names) => {
|
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
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* Copyright 2022 Chad Transtrum
|
|
2
|
+
*
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License.
|
|
5
|
+
* You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
* See the License for the specific language governing permissions and
|
|
13
|
+
* limitations under the License.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// CLI suggest command
|
|
17
|
+
|
|
18
|
+
import { suggest } from '@slugbugblue/trax/analyst'
|
|
19
|
+
|
|
20
|
+
export const suggestCmd = {
|
|
21
|
+
name: 'suggest',
|
|
22
|
+
alt: ['bot'],
|
|
23
|
+
desc: 'suggest a move',
|
|
24
|
+
help: ['Suggest a move in the current game.'],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
suggestCmd.fn = (CLI, debug) => {
|
|
28
|
+
if (!String(CLI.GAME?.id)) {
|
|
29
|
+
return CLI.error('No active game. Type "new" to start a game.')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const game = CLI.TRAX
|
|
33
|
+
const suggestion = suggest(game)
|
|
34
|
+
|
|
35
|
+
if (!suggestion.best) return
|
|
36
|
+
|
|
37
|
+
CLI.out(
|
|
38
|
+
CLI.bubble(
|
|
39
|
+
game.turn === 1 ? 'w' : 'b',
|
|
40
|
+
String(game.move + 1) + '. ' + suggestion.pick.move,
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if ('debug'.startsWith(debug)) {
|
|
45
|
+
const m = []
|
|
46
|
+
for (const move of suggestion.options) {
|
|
47
|
+
m.push(move.move + ' ' + move.score)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
CLI.out('[' + m.join(', ') + '] ' + suggestion.ms + 'ms')
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/cmds/view.js
CHANGED