@slugbugblue/trax-cli 0.12.0 → 0.13.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/.claude/settings.local.json +8 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +1 -1
- package/README.md +1 -1
- package/eslint.config.js +24 -0
- package/package.json +7 -18
- package/src/cli.js +246 -106
- package/src/cmds/analyze.js +72 -66
- package/src/cmds/delete.js +35 -43
- package/src/cmds/help.js +54 -57
- package/src/cmds/import-export.js +149 -122
- package/src/cmds/index.js +40 -0
- package/src/cmds/list.js +92 -87
- package/src/cmds/new.js +50 -55
- package/src/cmds/notes.js +20 -18
- package/src/cmds/play-try.js +147 -106
- package/src/cmds/puzzles.js +53 -39
- package/src/cmds/select.js +22 -31
- package/src/cmds/suggest.js +28 -37
- package/src/cmds/undo.js +31 -26
- package/src/cmds/view.js +42 -51
- package/src/utils.js +1 -1
- package/src/version.js +1 -1
- package/xo.config.js +10 -0
- package/src/types.d.ts +0 -33
package/src/cmds/new.js
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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.
|
|
1
|
+
/** CLI new command
|
|
2
|
+
* @copyright 2022-2026
|
|
3
|
+
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
|
+
* @license Apache-2.0
|
|
14
5
|
*/
|
|
15
6
|
|
|
16
|
-
|
|
7
|
+
/** @typedef {import('../cli.js').CLIContext} CLIContext */
|
|
17
8
|
|
|
18
9
|
export const newCmd = {
|
|
19
10
|
name: 'new',
|
|
@@ -25,6 +16,51 @@ export const newCmd = {
|
|
|
25
16
|
'Valid variants are "trax", "loop", and "8x8". Defaults to "trax" if not',
|
|
26
17
|
'specified. Use the "vs" keyword to use specific names for the players.',
|
|
27
18
|
],
|
|
19
|
+
/** @param {CLIContext} CLI @param {string} [variant] @param {...string} names */
|
|
20
|
+
fn(CLI, variant, ...names) {
|
|
21
|
+
let rules = ''
|
|
22
|
+
if (variant) {
|
|
23
|
+
variant = variant.toLowerCase()
|
|
24
|
+
if (variant.length > 1 && 'puzzles'.startsWith(variant)) {
|
|
25
|
+
return CLI.do('puzzles', 'new', ...names)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (['8x8', '8', 'trax8x8', '8trax', '8x8trax'].includes(variant)) {
|
|
29
|
+
rules = 'trax8'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (['loop', 'l', 'looptrax'].includes(variant)) rules = 'traxloop'
|
|
33
|
+
if (variant === 't') rules = 'trax'
|
|
34
|
+
if (['trax', 'traxloop', 'trax8'].includes(variant)) rules = variant
|
|
35
|
+
|
|
36
|
+
if (!rules) {
|
|
37
|
+
if (names.includes('vs') || names.includes('VS')) {
|
|
38
|
+
// We don't have a variant, we have player names
|
|
39
|
+
names.unshift(variant)
|
|
40
|
+
} else {
|
|
41
|
+
CLI.error('Unknown Trax variant.')
|
|
42
|
+
return CLI.do('help', 'new')
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const players = pairUp()
|
|
48
|
+
if (names.length > 0) {
|
|
49
|
+
let vs = names.indexOf('vs')
|
|
50
|
+
if (vs === -1) vs = names.indexOf('VS')
|
|
51
|
+
if (vs >= 0) {
|
|
52
|
+
players[0] = names.slice(0, vs).join(' ') || 'white'
|
|
53
|
+
players[1] = names.slice(vs + 1).join(' ') || 'black'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const id = CLI.newGame(
|
|
58
|
+
/** @type {import('../cli.js').TraxVariant} */ (rules || 'trax'),
|
|
59
|
+
players,
|
|
60
|
+
'',
|
|
61
|
+
)
|
|
62
|
+
CLI.out(CLI.color('Started new game #' + id))
|
|
63
|
+
},
|
|
28
64
|
}
|
|
29
65
|
|
|
30
66
|
// Okay, yes, this is silly
|
|
@@ -103,44 +139,3 @@ const pairUp = () => {
|
|
|
103
139
|
if (duo[0].toLowerCase() !== duo[0] && Math.random() < 0.5) duo.reverse()
|
|
104
140
|
return duo
|
|
105
141
|
}
|
|
106
|
-
|
|
107
|
-
newCmd.fn = (CLI, variant, ...names) => {
|
|
108
|
-
let rules = ''
|
|
109
|
-
if (variant) {
|
|
110
|
-
variant = variant.toLowerCase()
|
|
111
|
-
if (variant.length > 1 && 'puzzles'.startsWith(variant)) {
|
|
112
|
-
return CLI.do('puzzles', 'new', ...names)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (['8x8', '8', 'trax8x8', '8trax', '8x8trax'].includes(variant)) {
|
|
116
|
-
rules = 'trax8'
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (['loop', 'l', 'looptrax'].includes(variant)) rules = 'traxloop'
|
|
120
|
-
if (variant === 't') rules = 'trax'
|
|
121
|
-
if (['trax', 'traxloop', 'trax8'].includes(variant)) rules = variant
|
|
122
|
-
|
|
123
|
-
if (!rules) {
|
|
124
|
-
if (names.includes('vs') || names.includes('VS')) {
|
|
125
|
-
// We don't have a variant, we have player names
|
|
126
|
-
names.unshift(variant)
|
|
127
|
-
} else {
|
|
128
|
-
CLI.error('Unknown Trax variant.')
|
|
129
|
-
return CLI.do('help', 'new')
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const players = pairUp()
|
|
135
|
-
if (names.length > 0) {
|
|
136
|
-
let vs = names.indexOf('vs')
|
|
137
|
-
if (vs === -1) vs = names.indexOf('VS')
|
|
138
|
-
if (vs >= 0) {
|
|
139
|
-
players[0] = names.slice(0, vs).join(' ') || 'white'
|
|
140
|
-
players[1] = names.slice(vs + 1).join(' ') || 'black'
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const id = CLI.newGame(rules, players, '')
|
|
145
|
-
CLI.out(CLI.color('Started new game #' + id))
|
|
146
|
-
}
|
package/src/cmds/notes.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/** Notes CLI command.
|
|
2
|
-
* @copyright 2022
|
|
2
|
+
* @copyright 2022-2026
|
|
3
3
|
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
4
|
* @license Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/** @typedef {import('../cli.js').CLIContext} CLIContext */
|
|
8
|
+
|
|
7
9
|
export const notesCmd = {
|
|
8
10
|
name: 'notes',
|
|
9
11
|
args: '[#id] <text>',
|
|
@@ -12,26 +14,26 @@ export const notesCmd = {
|
|
|
12
14
|
'Add a short note to a game that will be shown when the game is',
|
|
13
15
|
'displayed, listed, or exported.',
|
|
14
16
|
],
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
17
|
+
/** @param {CLIContext} CLI @param {string} [id] @param {...string} note */
|
|
18
|
+
fn(CLI, id, ...note) {
|
|
19
|
+
const current = String(CLI.GAME.id)
|
|
20
|
+
id ||= current
|
|
21
|
+
if (!/^#?\d+$/v.test(id)) {
|
|
22
|
+
note.unshift(id)
|
|
23
|
+
id = current
|
|
24
|
+
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
if (id.startsWith('#')) id = id.slice(1)
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
const game = CLI.GAMES[id]
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
if (!game) return CLI.error('Game not found.')
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
const notes = game.notes || []
|
|
33
|
+
const move = game.moves.length > 0 ? game.moves.split(' ').length : 0
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
notes.push({ move, note: note.join(' ') })
|
|
36
|
+
game.notes = notes
|
|
37
|
+
CLI.save()
|
|
38
|
+
},
|
|
37
39
|
}
|
package/src/cmds/play-try.js
CHANGED
|
@@ -1,25 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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.
|
|
1
|
+
/** CLI play/try commands
|
|
2
|
+
* @copyright 2022-2026
|
|
3
|
+
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
|
+
* @license Apache-2.0
|
|
14
5
|
*/
|
|
15
6
|
|
|
16
|
-
|
|
7
|
+
/** @typedef {import('../cli.js').CLIContext} CLIContext */
|
|
17
8
|
|
|
18
9
|
import { suggest } from '@slugbugblue/trax-analyst'
|
|
10
|
+
import { puzzles } from '@slugbugblue/trax-puzzles'
|
|
19
11
|
import { timeString } from '@slugbugblue/trax-cli/utils'
|
|
20
12
|
|
|
21
|
-
const moveRx = /^\d+\.?$/
|
|
22
|
-
const notationRx = /^[@a-z]+\d+[bps
|
|
13
|
+
const moveRx = /^\d+\.?$/v
|
|
14
|
+
const notationRx = /^[@a-z]+\d+[bps\/\\+]$/iv
|
|
23
15
|
|
|
24
16
|
export const playCmd = {
|
|
25
17
|
name: 'play',
|
|
@@ -35,6 +27,65 @@ export const playCmd = {
|
|
|
35
27
|
'Multiple moves can be submitted at once. Move numbers are optional,',
|
|
36
28
|
'but if included, they will be checked for accuracy.',
|
|
37
29
|
],
|
|
30
|
+
/** @param {CLIContext} CLI @param {...string} moves */
|
|
31
|
+
fn(CLI, ...moves) {
|
|
32
|
+
if (moves.length === 0 || !moves[0]) {
|
|
33
|
+
CLI.error('You must provide a move.')
|
|
34
|
+
return CLI.do('help', 'play')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!CLI.GAME?.id) {
|
|
38
|
+
CLI.do('new')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const game = CLI.GAME
|
|
42
|
+
const trax = CLI.TRAX
|
|
43
|
+
|
|
44
|
+
if (trax.over) {
|
|
45
|
+
return CLI.error('The game is over.')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const start = trax.move
|
|
49
|
+
let checkMove
|
|
50
|
+
|
|
51
|
+
for (let move of moves) {
|
|
52
|
+
if (notationRx.test(move)) {
|
|
53
|
+
const moveNumber = trax.move
|
|
54
|
+
move = CLI.fixNotation(move)
|
|
55
|
+
if (checkMove) {
|
|
56
|
+
trax.play(checkMove, move)
|
|
57
|
+
} else {
|
|
58
|
+
trax.play(move)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (trax.move === moveNumber) {
|
|
62
|
+
CLI.error(
|
|
63
|
+
'Move "' +
|
|
64
|
+
(checkMove ? checkMove + '. ' : '') +
|
|
65
|
+
move +
|
|
66
|
+
'" is invalid.',
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
} else if (moveRx.test(move)) {
|
|
70
|
+
checkMove = Number(move)
|
|
71
|
+
} else if (move) {
|
|
72
|
+
CLI.error('Move "' + move + '" is not in the correct notation.')
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (trax.move !== start) {
|
|
77
|
+
const result = bot(CLI)
|
|
78
|
+
CLI.updateGameData()
|
|
79
|
+
CLI.do('view')
|
|
80
|
+
if (result === 'lose') {
|
|
81
|
+
CLI.error('Failed to complete puzzle ' + game.puzzle)
|
|
82
|
+
} else if (result === 'win') {
|
|
83
|
+
CLI.out(CLI.color.success('You completed puzzle ' + game.puzzle))
|
|
84
|
+
} else if (result) {
|
|
85
|
+
CLI.out(result)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
38
89
|
}
|
|
39
90
|
|
|
40
91
|
export const tryCmd = {
|
|
@@ -47,15 +98,80 @@ export const tryCmd = {
|
|
|
47
98
|
'View a move without committing to it. This is useful for seeing the',
|
|
48
99
|
'effects of forced tiles.',
|
|
49
100
|
],
|
|
101
|
+
/** @param {CLIContext} CLI @param {string} [move] */
|
|
102
|
+
fn(CLI, move) {
|
|
103
|
+
if (!move || move.length === 0) {
|
|
104
|
+
CLI.error('You must provide a move.')
|
|
105
|
+
return CLI.do('help', 'try')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!CLI.TRAX) {
|
|
109
|
+
CLI.do('new')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (CLI.TRAX.over) {
|
|
113
|
+
return CLI.error('The game is over.')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (notationRx.test(move)) {
|
|
117
|
+
move = CLI.fixNotation(move)
|
|
118
|
+
const play = CLI.TRAX.dropTile(move, undefined, 'tentative')
|
|
119
|
+
if (play.valid || play.dropped.length > 0) {
|
|
120
|
+
CLI.display(CLI.TRAX, CLI.GAME.players, move)
|
|
121
|
+
} else {
|
|
122
|
+
CLI.error('Move is invalid.')
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
CLI.error('Move is invalid.')
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if a concrete move matches a defense key token.
|
|
132
|
+
* Handles the catch-all wildcard (*), column wildcards (@**, A**, etc.),
|
|
133
|
+
* and row wildcards (*0*, *5*, etc.).
|
|
134
|
+
* @param {string} move
|
|
135
|
+
* @param {string} token
|
|
136
|
+
* @returns {boolean}
|
|
137
|
+
*/
|
|
138
|
+
const matchesToken = (move, token) => {
|
|
139
|
+
if (token === '*') return true
|
|
140
|
+
if (!token.includes('*')) return token === move
|
|
141
|
+
const pattern =
|
|
142
|
+
'^' +
|
|
143
|
+
token
|
|
144
|
+
.replace('*', token.startsWith('*') ? '[@A-Za-z]+' : '[0-9]+')
|
|
145
|
+
.replace('*', String.raw`[+/\\]`) +
|
|
146
|
+
'$'
|
|
147
|
+
return new RegExp(pattern, 'v').test(move)
|
|
50
148
|
}
|
|
51
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Find the defense move for the current puzzle state, if one exists.
|
|
152
|
+
* @param {import('@slugbugblue/trax-puzzles').Defense} defense
|
|
153
|
+
* @param {string[]} moves
|
|
154
|
+
* @returns {string | undefined}
|
|
155
|
+
*/
|
|
156
|
+
const getDefenseMove = (defense, moves) => {
|
|
157
|
+
const key = Object.keys(defense).find((k) => {
|
|
158
|
+
const tokens = k.split(' ')
|
|
159
|
+
return (
|
|
160
|
+
tokens.length === moves.length &&
|
|
161
|
+
tokens.every((token, i) => matchesToken(moves[i], token))
|
|
162
|
+
)
|
|
163
|
+
})
|
|
164
|
+
return key ? defense[key] : undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** @param {CLIContext} CLI @returns {string} */
|
|
52
168
|
const bot = (CLI) => {
|
|
53
169
|
const game = CLI.GAME
|
|
54
170
|
const trax = CLI.TRAX
|
|
55
171
|
const players = game.players || ['a', 'b']
|
|
56
172
|
const name = players[trax.turn - 1]
|
|
57
173
|
if (trax.over && game.puzzle) {
|
|
58
|
-
if (trax.move <= game.max && name !== 'puzzlebot') {
|
|
174
|
+
if (trax.move <= (game.max ?? 0) && name !== 'puzzlebot') {
|
|
59
175
|
CLI.puzzleSolved(game.puzzle)
|
|
60
176
|
return 'win'
|
|
61
177
|
}
|
|
@@ -63,8 +179,8 @@ const bot = (CLI) => {
|
|
|
63
179
|
return 'lose'
|
|
64
180
|
}
|
|
65
181
|
|
|
66
|
-
if (!trax.over && /bot\b/
|
|
67
|
-
if (game.puzzle && trax.move >= game.max) {
|
|
182
|
+
if (!trax.over && /bot\b/iv.test(name)) {
|
|
183
|
+
if (game.puzzle && game.max !== undefined && trax.move >= game.max) {
|
|
68
184
|
trax.play('puzzled')
|
|
69
185
|
return 'lose'
|
|
70
186
|
}
|
|
@@ -72,7 +188,18 @@ const bot = (CLI) => {
|
|
|
72
188
|
CLI.do('view')
|
|
73
189
|
CLI.out(CLI.bubble(trax.color + 'd', `${name} thinking...`))
|
|
74
190
|
const start = Date.now()
|
|
75
|
-
|
|
191
|
+
|
|
192
|
+
let move
|
|
193
|
+
if (game.puzzle) {
|
|
194
|
+
const puzzle = puzzles.find((p) => p.id === game.puzzle)
|
|
195
|
+
if (puzzle?.defense) {
|
|
196
|
+
const offset = puzzle.notation.split(' ').length
|
|
197
|
+
move = getDefenseMove(puzzle.defense, trax.moves.slice(offset))
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
move ??= suggest(trax)?.pick?.move
|
|
202
|
+
|
|
76
203
|
const ms = Date.now() - start
|
|
77
204
|
if (move) {
|
|
78
205
|
CLI.out(
|
|
@@ -87,89 +214,3 @@ const bot = (CLI) => {
|
|
|
87
214
|
|
|
88
215
|
return ''
|
|
89
216
|
}
|
|
90
|
-
|
|
91
|
-
playCmd.fn = (CLI, ...moves) => {
|
|
92
|
-
if (moves.length === 0 || !moves[0]) {
|
|
93
|
-
CLI.error('You must provide a move.')
|
|
94
|
-
return CLI.do('help', 'play')
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (!CLI.GAME?.id) {
|
|
98
|
-
CLI.do('new')
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const game = CLI.GAME
|
|
102
|
-
const trax = CLI.TRAX
|
|
103
|
-
|
|
104
|
-
if (trax.over) {
|
|
105
|
-
return CLI.error('The game is over.')
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const start = trax.move
|
|
109
|
-
let checkMove
|
|
110
|
-
|
|
111
|
-
for (let move of moves) {
|
|
112
|
-
if (notationRx.test(move)) {
|
|
113
|
-
const moveNumber = trax.move
|
|
114
|
-
move = CLI.fixNotation(move)
|
|
115
|
-
if (checkMove) {
|
|
116
|
-
trax.play(checkMove, move)
|
|
117
|
-
} else {
|
|
118
|
-
trax.play(move)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (trax.move === moveNumber) {
|
|
122
|
-
CLI.error(
|
|
123
|
-
'Move "' +
|
|
124
|
-
(checkMove ? checkMove + '. ' : '') +
|
|
125
|
-
move +
|
|
126
|
-
'" is invalid.',
|
|
127
|
-
)
|
|
128
|
-
}
|
|
129
|
-
} else if (moveRx.test(move)) {
|
|
130
|
-
checkMove = Number(move)
|
|
131
|
-
} else if (move) {
|
|
132
|
-
CLI.error('Move "' + move + '" is not in the correct notation.')
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (trax.move !== start) {
|
|
137
|
-
const result = bot(CLI)
|
|
138
|
-
CLI.updateGameData()
|
|
139
|
-
CLI.do('view')
|
|
140
|
-
if (result === 'lose') {
|
|
141
|
-
CLI.error('Failed to complete puzzle ' + game.puzzle)
|
|
142
|
-
} else if (result === 'win') {
|
|
143
|
-
CLI.out(CLI.color.success('You completed puzzle ' + game.puzzle))
|
|
144
|
-
} else if (result) {
|
|
145
|
-
CLI.out(result)
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
tryCmd.fn = (CLI, move) => {
|
|
151
|
-
if (!move || move.length === 0) {
|
|
152
|
-
CLI.error('You must provide a move.')
|
|
153
|
-
return CLI.do('help', 'try')
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!CLI.TRAX) {
|
|
157
|
-
CLI.do('new')
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (CLI.TRAX.over) {
|
|
161
|
-
return CLI.error('The game is over.')
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (notationRx.test(move)) {
|
|
165
|
-
move = CLI.fixNotation(move)
|
|
166
|
-
const play = CLI.TRAX.dropTile(move, undefined, 'tentative')
|
|
167
|
-
if (play.valid || play.dropped.length > 0) {
|
|
168
|
-
CLI.display(CLI.TRAX, CLI.GAME.players, move)
|
|
169
|
-
} else {
|
|
170
|
-
CLI.error('Move is invalid.')
|
|
171
|
-
}
|
|
172
|
-
} else {
|
|
173
|
-
CLI.error('Move is invalid.')
|
|
174
|
-
}
|
|
175
|
-
}
|
package/src/cmds/puzzles.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/** @file Puzzles CLI
|
|
2
|
-
* @copyright 2022-
|
|
2
|
+
* @copyright 2022-2026
|
|
3
3
|
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
4
|
* @license Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/** @typedef {import('../cli.js').CLIContext} CLIContext */
|
|
8
|
+
|
|
7
9
|
import { Trax } from '@slugbugblue/trax'
|
|
8
10
|
import { levels, puzzles, sources } from '@slugbugblue/trax-puzzles'
|
|
9
11
|
|
|
@@ -29,13 +31,50 @@ export const puzzlesCmd = {
|
|
|
29
31
|
'Practice your skills with puzzles. Use "list" to see the puzzle',
|
|
30
32
|
'categories, or "start" to play a puzzle.',
|
|
31
33
|
],
|
|
34
|
+
/** @param {CLIContext} CLI @param {string} [action] @param {string} [id] */
|
|
35
|
+
fn(CLI, action, id) {
|
|
36
|
+
action = action?.toLowerCase() ?? 'list'
|
|
37
|
+
if (startAliases.some((alias) => alias.startsWith(action))) {
|
|
38
|
+
if (!id) return startNextPuzzle(CLI)
|
|
39
|
+
|
|
40
|
+
const puzzle = puzzles.find((p) => p.id === id)
|
|
41
|
+
if (puzzle) {
|
|
42
|
+
return startPuzzle(CLI, puzzle)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (validLevels.includes(id)) {
|
|
46
|
+
return startNextPuzzle(CLI, levels.indexOf(id))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const possibles = puzzles.filter((p) => p.id.includes(id))
|
|
50
|
+
if (possibles.length === 1) {
|
|
51
|
+
return startPuzzle(CLI, possibles[0])
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return CLI.error('Could not find puzzle ' + id)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (action) {
|
|
58
|
+
const x = id || action
|
|
59
|
+
if (validLevels.includes(x)) return listLevel(CLI, x)
|
|
60
|
+
if (listAliases.some((alias) => alias.startsWith(x))) {
|
|
61
|
+
return listPuzzles(CLI)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return showPuzzle(CLI, x)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
listPuzzles(CLI)
|
|
68
|
+
},
|
|
32
69
|
}
|
|
33
70
|
|
|
71
|
+
/** @param {string} [word] @returns {string} */
|
|
34
72
|
const an = (word = '') => {
|
|
35
|
-
const a = /^[aeiou]
|
|
73
|
+
const a = /^[aeiou]/v.test(word) ? 'an' : 'a'
|
|
36
74
|
return a + ' ' + word
|
|
37
75
|
}
|
|
38
76
|
|
|
77
|
+
/** @param {CLIContext} CLI @param {string} id @param {boolean} [started] */
|
|
39
78
|
const showPuzzle = (CLI, id, started = false) => {
|
|
40
79
|
let puzzle = puzzles.find((p) => p.id === id)
|
|
41
80
|
if (!puzzle) {
|
|
@@ -80,6 +119,7 @@ const showPuzzle = (CLI, id, started = false) => {
|
|
|
80
119
|
)
|
|
81
120
|
}
|
|
82
121
|
|
|
122
|
+
/** @param {CLIContext} CLI */
|
|
83
123
|
const listPuzzles = (CLI) => {
|
|
84
124
|
let size = 1
|
|
85
125
|
const data = []
|
|
@@ -112,6 +152,7 @@ const listPuzzles = (CLI) => {
|
|
|
112
152
|
}
|
|
113
153
|
}
|
|
114
154
|
|
|
155
|
+
/** @param {CLIContext} CLI @param {string} levelName */
|
|
115
156
|
const listLevel = (CLI, levelName) => {
|
|
116
157
|
let size = 1
|
|
117
158
|
const level = levels.indexOf(levelName)
|
|
@@ -134,55 +175,28 @@ const listLevel = (CLI, levelName) => {
|
|
|
134
175
|
}
|
|
135
176
|
}
|
|
136
177
|
|
|
178
|
+
/** @param {CLIContext} CLI @param {import('@slugbugblue/trax-puzzles').Puzzle} puzzle */
|
|
137
179
|
const startPuzzle = (CLI, puzzle) => {
|
|
138
180
|
const id = CLI.newPuzzle(puzzle)
|
|
139
181
|
CLI.out(CLI.color(`Started puzzle ${puzzle.id} as game #${id}`))
|
|
140
182
|
showPuzzle(CLI, puzzle.id, true)
|
|
141
183
|
}
|
|
142
184
|
|
|
185
|
+
/** @param {CLIContext} CLI @param {number} [level] */
|
|
143
186
|
const startNextPuzzle = (CLI, level) => {
|
|
144
187
|
if (level === undefined) level = CLI.puzzleLevel
|
|
145
|
-
const unsolved = puzzles
|
|
188
|
+
const unsolved = puzzles
|
|
189
|
+
.filter((p) => !CLI.puzzle(p.id).solved)
|
|
190
|
+
.toSorted(
|
|
191
|
+
(a, b) =>
|
|
192
|
+
a.level - b.level ||
|
|
193
|
+
a.notation.split(' ').length - b.notation.split(' ').length,
|
|
194
|
+
)
|
|
146
195
|
if (unsolved.length === 0) {
|
|
147
|
-
CLI.out(CLI.success('You have already completed all the puzzles.'))
|
|
196
|
+
CLI.out(CLI.color.success('You have already completed all the puzzles.'))
|
|
148
197
|
return CLI.error('Specify a specific puzzle to try one again.')
|
|
149
198
|
}
|
|
150
199
|
|
|
151
200
|
const next = unsolved.find((p) => p.level >= level) || unsolved[0]
|
|
152
201
|
startPuzzle(CLI, next)
|
|
153
202
|
}
|
|
154
|
-
|
|
155
|
-
puzzlesCmd.fn = (CLI, action, id) => {
|
|
156
|
-
action = action?.toLowerCase() ?? 'list'
|
|
157
|
-
if (startAliases.some((alias) => alias.startsWith(action))) {
|
|
158
|
-
if (!id) return startNextPuzzle(CLI)
|
|
159
|
-
|
|
160
|
-
const puzzle = puzzles.find((p) => p.id === id)
|
|
161
|
-
if (puzzle) {
|
|
162
|
-
return startPuzzle(CLI, puzzle)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (validLevels.includes(id)) {
|
|
166
|
-
return startNextPuzzle(CLI, levels.indexOf(id))
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const possibles = puzzles.filter((p) => p.id.includes(id))
|
|
170
|
-
if (possibles.length === 1) {
|
|
171
|
-
return startPuzzle(CLI, possibles[0])
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return CLI.error('Could not find puzzle ' + id)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (action) {
|
|
178
|
-
const x = id || action
|
|
179
|
-
if (validLevels.includes(x)) return listLevel(CLI, x)
|
|
180
|
-
if (listAliases.some((alias) => alias.startsWith(x))) {
|
|
181
|
-
return listPuzzles(CLI)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return showPuzzle(CLI, x)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
listPuzzles(CLI)
|
|
188
|
-
}
|
package/src/cmds/select.js
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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.
|
|
1
|
+
/** CLI select command
|
|
2
|
+
* @copyright 2022-2026
|
|
3
|
+
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
|
+
* @license Apache-2.0
|
|
14
5
|
*/
|
|
15
6
|
|
|
16
|
-
|
|
7
|
+
/** @typedef {import('../cli.js').CLIContext} CLIContext */
|
|
17
8
|
|
|
18
9
|
export const selectCmd = {
|
|
19
10
|
name: 'select',
|
|
@@ -21,27 +12,27 @@ export const selectCmd = {
|
|
|
21
12
|
args: '#id [command]',
|
|
22
13
|
comp: '<id> <cmd>',
|
|
23
14
|
desc: 'select a different game',
|
|
24
|
-
rx: /^#\d
|
|
15
|
+
rx: /^#\d+$/v,
|
|
25
16
|
help: [
|
|
26
17
|
'Make another game the default game of future commands. If another',
|
|
27
18
|
'command is included, that command will be run immediately.',
|
|
28
19
|
],
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
20
|
+
/** @param {CLIContext} CLI @param {string} [id] @param {...string} cmds */
|
|
21
|
+
fn(CLI, id, ...cmds) {
|
|
22
|
+
if (!id) {
|
|
23
|
+
CLI.error('Missing id.')
|
|
24
|
+
return CLI.do('help', 'select')
|
|
25
|
+
}
|
|
36
26
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
27
|
+
if (id.startsWith('#')) id = id.slice(1)
|
|
28
|
+
CLI.load(id)
|
|
29
|
+
if (String(CLI.GAME?.id) === id) {
|
|
30
|
+
CLI.out(CLI.color(`Game #${id} selected.`))
|
|
31
|
+
if (cmds.length > 0) {
|
|
32
|
+
CLI.doNext(cmds.join(' '))
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
CLI.error('Invalid id. Type "list" to see available games.')
|
|
43
36
|
}
|
|
44
|
-
}
|
|
45
|
-
CLI.error('Invalid id. Type "list" to see available games.')
|
|
46
|
-
}
|
|
37
|
+
},
|
|
47
38
|
}
|