@slugbugblue/trax 0.15.0 → 0.17.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 +14 -0
- package/README.md +8 -7
- package/package.json +1 -1
- package/src/analyst.js +1 -1
- package/src/cli.js +29 -22
- package/src/cmds/import-export.js +137 -25
- package/src/cmds/list.js +29 -7
- package/src/cmds/notes.js +34 -0
- package/src/cmds/play-try.js +1 -1
- package/src/cmds/view.js +6 -1
- package/src/engine.js +1 -1
- package/src/threats.js +1 -0
- package/src/types.d.ts +9 -0
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @slugbugblue/trax
|
|
2
2
|
|
|
3
|
+
## 0.17.0 - 2022-12-29
|
|
4
|
+
|
|
5
|
+
- The `import` CLI command now loads comments from a .trx file
|
|
6
|
+
|
|
7
|
+
## 0.16.0 - 2022-12-27
|
|
8
|
+
|
|
9
|
+
- Added a `notes` command to the CLI to add comments to a game
|
|
10
|
+
|
|
11
|
+
- all comments are included in the output generated by the `export` command
|
|
12
|
+
- the latest comment can be used as a text filter by the `list` command
|
|
13
|
+
- the latest comment is displayed by the `view` and `list` commands
|
|
14
|
+
|
|
15
|
+
- Minor analysis improvements
|
|
16
|
+
|
|
3
17
|
## 0.15.0 - 2022-12-14
|
|
4
18
|
|
|
5
19
|
- Added shared `puzzles` commands to the CLI:
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Trax is a two-player boardless board game invented by David Smith. For more
|
|
|
13
13
|
information about Trax, including its [rules][traxrules] and
|
|
14
14
|
[history][traxhistory], see the official website at [traxgame.com][traxgame].
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
The goal of this javascript project is to allow Trax to be played
|
|
17
17
|
programmatically. It can be used as an engine in a web browser, for example at
|
|
18
18
|
[slugbugblue.com][sbb]; or in nodejs, for example by using the `trax` CLI
|
|
19
19
|
provided as part of this project.
|
|
@@ -27,7 +27,7 @@ changes to the engine should meet the same standard.
|
|
|
27
27
|
# Install into a javascript project
|
|
28
28
|
npm install @slugbugblue/trax
|
|
29
29
|
|
|
30
|
-
# Or install globally to use the trax CLI
|
|
30
|
+
# Or install globally to use the trax CLI more easily
|
|
31
31
|
npm install -g @slugbugblue/trax
|
|
32
32
|
```
|
|
33
33
|
|
|
@@ -43,11 +43,12 @@ avoid worries about the need to escape certain characters from the shell, and it
|
|
|
43
43
|
can be easier to run multiple commands back to back.
|
|
44
44
|
|
|
45
45
|
Help is available for the CLI through the use of the `help` command. Type
|
|
46
|
-
`trax help`
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
`trax help` (or `npx trax help` with a local installation, or simply `help` if
|
|
47
|
+
run interactively) to see a list of the available commands, and `trax help play`
|
|
48
|
+
to see information about the `play` command. Aliases and abbreviations are
|
|
49
|
+
provided to minimize typing and to try to avoid the requirement of memorizing
|
|
50
|
+
the commands. For example, the `list` command has an alias of `ls`, and the
|
|
51
|
+
`select` command can be activated using `#`.
|
|
51
52
|
|
|
52
53
|
When you first launch the CLI, you can create a new game using the `new`
|
|
53
54
|
command, and then enter moves for that game using `play`. Since `play` is likely
|
package/package.json
CHANGED
package/src/analyst.js
CHANGED
|
@@ -379,7 +379,7 @@ export const Analysis = class {
|
|
|
379
379
|
const calcLevel = level > 0 && defending ? level + threatLevel - 1 : level
|
|
380
380
|
let points = attackScore(inactive, level, calc ? calcLevel : 0)
|
|
381
381
|
if (level === threatLevel) points = passive ? Math.floor(points / 2) : 0
|
|
382
|
-
if (threatening) points = Math.floor(points / 2)
|
|
382
|
+
if (passive && threatening) points = Math.floor(points / 2)
|
|
383
383
|
score += points
|
|
384
384
|
}
|
|
385
385
|
|
package/src/cli.js
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
} from './cmds/import-export.js'
|
|
39
39
|
import { listCmd } from './cmds/list.js'
|
|
40
40
|
import { newCmd } from './cmds/new.js'
|
|
41
|
+
import { notesCmd } from './cmds/notes.js'
|
|
41
42
|
import { playCmd, tryCmd } from './cmds/play-try.js'
|
|
42
43
|
import { puzzlesCmd } from './cmds/puzzles.js'
|
|
43
44
|
import { selectCmd } from './cmds/select.js'
|
|
@@ -87,6 +88,7 @@ const plugins = [
|
|
|
87
88
|
importCmd,
|
|
88
89
|
listCmd,
|
|
89
90
|
newCmd,
|
|
91
|
+
notesCmd,
|
|
90
92
|
playCmd,
|
|
91
93
|
puzzlesCmd,
|
|
92
94
|
quitCmd,
|
|
@@ -197,14 +199,6 @@ const DATA = { games: {}, puzzles: {} }
|
|
|
197
199
|
|
|
198
200
|
// CLI context object for plugins
|
|
199
201
|
const CLI = {
|
|
200
|
-
// Notation manipulation
|
|
201
|
-
fixNotation(move) {
|
|
202
|
-
if (move.endsWith('b')) return move.slice(0, -1) + '\\'
|
|
203
|
-
if (move.endsWith('s')) return move.slice(0, -1) + '/'
|
|
204
|
-
if (move.endsWith('p')) return move.slice(0, -1) + '+'
|
|
205
|
-
return move
|
|
206
|
-
},
|
|
207
|
-
|
|
208
202
|
// Quick access to all stored games
|
|
209
203
|
get GAMES() {
|
|
210
204
|
return DATA.games || {}
|
|
@@ -283,6 +277,28 @@ const CLI = {
|
|
|
283
277
|
CLI.out(CLI.color.error(text))
|
|
284
278
|
},
|
|
285
279
|
|
|
280
|
+
// Notation manipulation
|
|
281
|
+
fixNotation(move) {
|
|
282
|
+
if (move.endsWith('b')) return move.slice(0, -1) + '\\'
|
|
283
|
+
if (move.endsWith('s')) return move.slice(0, -1) + '/'
|
|
284
|
+
if (move.endsWith('p')) return move.slice(0, -1) + '+'
|
|
285
|
+
return move
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
load(id) {
|
|
289
|
+
if (!id) id = Object.keys(DATA.games || {})[0]
|
|
290
|
+
if (id && DATA.games?.[id] && DATA.games[id].rules) {
|
|
291
|
+
if (id !== String(CONFIG.id)) {
|
|
292
|
+
CONFIG.id = id
|
|
293
|
+
saveConfig()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const game = CLI.GAME
|
|
297
|
+
if (!game.players) game.players = ['white', 'black']
|
|
298
|
+
CLI.TRAX = new Trax(game.rules, game.moves, 'cli')
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
|
|
286
302
|
newGame(rules, players, moves) {
|
|
287
303
|
// Creates a new game
|
|
288
304
|
const id = newId(DATA)
|
|
@@ -367,6 +383,11 @@ const CLI = {
|
|
|
367
383
|
return undefined
|
|
368
384
|
},
|
|
369
385
|
|
|
386
|
+
/** Write out the current data to disk. */
|
|
387
|
+
save() {
|
|
388
|
+
saveData()
|
|
389
|
+
},
|
|
390
|
+
|
|
370
391
|
setGame(moves) {
|
|
371
392
|
// Updates the moves for the current game
|
|
372
393
|
const game = CLI.GAME
|
|
@@ -390,20 +411,6 @@ const CLI = {
|
|
|
390
411
|
}
|
|
391
412
|
},
|
|
392
413
|
|
|
393
|
-
load(id) {
|
|
394
|
-
if (!id) id = Object.keys(DATA.games || {})[0]
|
|
395
|
-
if (id && DATA.games?.[id] && DATA.games[id].rules) {
|
|
396
|
-
if (id !== String(CONFIG.id)) {
|
|
397
|
-
CONFIG.id = id
|
|
398
|
-
saveConfig()
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const game = CLI.GAME
|
|
402
|
-
if (!game.players) game.players = ['white', 'black']
|
|
403
|
-
CLI.TRAX = new Trax(game.rules, game.moves, 'cli')
|
|
404
|
-
}
|
|
405
|
-
},
|
|
406
|
-
|
|
407
414
|
// Pass through functions
|
|
408
415
|
bubble: tty.bubble,
|
|
409
416
|
display: tty.display,
|
|
@@ -137,6 +137,79 @@ export const exportCmd = {
|
|
|
137
137
|
],
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/** Interpret the rules string of a .trx file.
|
|
141
|
+
* @arg {string} r - rules string in lower case
|
|
142
|
+
* @returns {TraxVariant}
|
|
143
|
+
*/
|
|
144
|
+
const getRules = (r) =>
|
|
145
|
+
r.includes('loop') ? 'traxloop' : r.includes('8x8') ? 'trax8' : 'trax'
|
|
146
|
+
|
|
147
|
+
/** Decode a .trx formatted string into a game, preserving players and comments.
|
|
148
|
+
* @arg {any} CLI - the CLI object
|
|
149
|
+
* @arg {string} content - the contents of the .trx file
|
|
150
|
+
* @returns {boolean} - true if a game was created
|
|
151
|
+
*/
|
|
152
|
+
const interpretFile = (CLI, content) => {
|
|
153
|
+
let rules
|
|
154
|
+
let trax
|
|
155
|
+
let players = []
|
|
156
|
+
const moves = []
|
|
157
|
+
const comments = []
|
|
158
|
+
for (const line of content.split('\n')) {
|
|
159
|
+
if (!line) continue
|
|
160
|
+
const [ln, ...comment] = line.split(/[#;]/)
|
|
161
|
+
if (ln) {
|
|
162
|
+
const lower = ln.toLowerCase()
|
|
163
|
+
if (!rules && lower.includes('trax')) {
|
|
164
|
+
rules = getRules(lower)
|
|
165
|
+
} else if (players.length === 0 && lower.includes(' vs ')) {
|
|
166
|
+
players = ln.split(' ').filter(Boolean)
|
|
167
|
+
} else {
|
|
168
|
+
moves.push(...ln.split(' '))
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!trax && rules && (players.length > 0 || moves.length > 0)) {
|
|
173
|
+
trax = new Trax(rules)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (moves.length > 0) {
|
|
177
|
+
if (!trax) return false
|
|
178
|
+
if (trax.gameOver) {
|
|
179
|
+
comments.push({
|
|
180
|
+
note: moves.map((c) => c.trim()).join(' '),
|
|
181
|
+
move: trax.move,
|
|
182
|
+
})
|
|
183
|
+
} else {
|
|
184
|
+
trax.playMoves(moves.filter(Boolean))
|
|
185
|
+
moves.splice(0, moves.length)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (comment.length > 0) {
|
|
190
|
+
const note = comment.map((c) => c.trim()).join(' ')
|
|
191
|
+
if (!/^@slugbugblue\/trax cli\.js v/.test(note)) {
|
|
192
|
+
comments.push({
|
|
193
|
+
note,
|
|
194
|
+
move: trax?.move || 0,
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!trax) return false
|
|
201
|
+
|
|
202
|
+
CLI.do('new', rules, ...players)
|
|
203
|
+
CLI.GAMES[String(CLI.GAME.id)].notes = comments
|
|
204
|
+
if (trax.moves.length > 0) {
|
|
205
|
+
CLI.do('play', ...trax.moves)
|
|
206
|
+
} else {
|
|
207
|
+
CLI.save()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
|
|
140
213
|
importCmd.fn = async (CLI, filename) => {
|
|
141
214
|
cli = CLI
|
|
142
215
|
if (!filename) {
|
|
@@ -156,36 +229,74 @@ importCmd.fn = async (CLI, filename) => {
|
|
|
156
229
|
}
|
|
157
230
|
}
|
|
158
231
|
|
|
159
|
-
if (!content) CLI.error(fsError.toString())
|
|
232
|
+
if (!content) return CLI.error(fsError.toString())
|
|
160
233
|
}
|
|
161
234
|
|
|
162
|
-
if (content) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
235
|
+
if (!interpretFile(CLI, content)) {
|
|
236
|
+
CLI.error(filename + ' does not appear to be formatted correctly.')
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Get all the notes for a certain move.
|
|
241
|
+
* @arg {number} move - The move number
|
|
242
|
+
* @arg {GameNotes} notes? - The notes for a game
|
|
243
|
+
* @returns {string} - Notes formatted for this move, or empty string if none
|
|
244
|
+
*/
|
|
245
|
+
const gameNotes = (move, notes) => {
|
|
246
|
+
notes = notes || []
|
|
247
|
+
const moveNotes = notes.filter((n) => n.move === move)
|
|
248
|
+
if (moveNotes.length === 0) return ''
|
|
249
|
+
return (
|
|
250
|
+
'; ' +
|
|
251
|
+
moveNotes.map((n) => n.note.replaceAll('\n', '\n; ')).join('\n; ') +
|
|
252
|
+
'\n'
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Join two strings together with a character with the appropriate joining
|
|
257
|
+
* character depending on the length of the strings.
|
|
258
|
+
* @arg {string} a - the first string
|
|
259
|
+
* @arg {string} b - the second string
|
|
260
|
+
* @returns {string} - both strings correctly joined
|
|
261
|
+
*/
|
|
262
|
+
const join = (a, b) =>
|
|
263
|
+
a.length === 0 ? b : a.length + b.length > 78 ? a + '\n' + b : a + ' ' + b
|
|
264
|
+
|
|
265
|
+
/** Interleave the game moves with any notes, formatted for the export file.
|
|
266
|
+
* @arg {string[]} moves - an array of move notations
|
|
267
|
+
* @arg {GameNotes} notes? - The notes for a game
|
|
268
|
+
* @returns {string} - A string that can be placed into an export file
|
|
269
|
+
*/
|
|
270
|
+
const interleaveNotation = (moves, notes) => {
|
|
271
|
+
let content = ''
|
|
272
|
+
let moveNumber = 0
|
|
273
|
+
let annotation = ''
|
|
274
|
+
for (const move of moves) {
|
|
275
|
+
const comments = gameNotes(moveNumber, notes)
|
|
276
|
+
if (comments) {
|
|
277
|
+
content += join(annotation, comments)
|
|
278
|
+
annotation = ''
|
|
180
279
|
}
|
|
181
280
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
281
|
+
moveNumber += 1
|
|
282
|
+
const notation = String(moveNumber) + '. ' + move
|
|
283
|
+
if (annotation.length + notation.length < 79) {
|
|
284
|
+
annotation += (annotation.length > 0 ? ' ' : '') + notation
|
|
185
285
|
} else {
|
|
186
|
-
|
|
286
|
+
content += '\n' + annotation
|
|
287
|
+
annotation = notation
|
|
187
288
|
}
|
|
188
289
|
}
|
|
290
|
+
|
|
291
|
+
const comments = gameNotes(moveNumber, notes)
|
|
292
|
+
if (comments) {
|
|
293
|
+
content += join(annotation, comments)
|
|
294
|
+
annotation = ''
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
content += annotation
|
|
298
|
+
if (!content.endsWith('\n')) content += '\n'
|
|
299
|
+
return content
|
|
189
300
|
}
|
|
190
301
|
|
|
191
302
|
exportCmd.fn = (CLI, id, filename) => {
|
|
@@ -218,7 +329,7 @@ exportCmd.fn = (CLI, id, filename) => {
|
|
|
218
329
|
content += '; Puzzle ' + game.puzzle
|
|
219
330
|
if (source) {
|
|
220
331
|
content += ' provided courtesy of ' + source.name
|
|
221
|
-
if (source.url) content += '
|
|
332
|
+
if (source.url) content += '\n; ' + source.url
|
|
222
333
|
}
|
|
223
334
|
|
|
224
335
|
if (puzzle.title) content += '\n; ' + puzzle.title
|
|
@@ -230,7 +341,8 @@ exportCmd.fn = (CLI, id, filename) => {
|
|
|
230
341
|
}
|
|
231
342
|
}
|
|
232
343
|
|
|
233
|
-
content += trax.
|
|
344
|
+
content += interleaveNotation(trax.moves, game.notes)
|
|
345
|
+
|
|
234
346
|
if (filename) filename = filename.replace(/[^ a-z\d.~/]/g, '')
|
|
235
347
|
if (filename) {
|
|
236
348
|
const fname = filename.split('/').pop()
|
package/src/cmds/list.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
// CLI list command
|
|
17
17
|
|
|
18
|
+
import process from 'node:process'
|
|
19
|
+
|
|
18
20
|
import { Trax } from '@slugbugblue/trax'
|
|
19
21
|
|
|
20
22
|
export const listCmd = {
|
|
@@ -35,7 +37,7 @@ export const listCmd = {
|
|
|
35
37
|
' #id: match a game or a certain number of moves',
|
|
36
38
|
' "active" or "over": whether a game is in progress or not',
|
|
37
39
|
' "loop" or "8x8" or "trax": filter by game type',
|
|
38
|
-
" <other text>: match a player's name",
|
|
40
|
+
" <other text>: match a player's name or the latest game note",
|
|
39
41
|
'Add the "reverse" option to reverse the sort order.',
|
|
40
42
|
],
|
|
41
43
|
}
|
|
@@ -80,14 +82,16 @@ const listSort = (list, filters) => {
|
|
|
80
82
|
} else if (['over', 'active'].includes(by)) {
|
|
81
83
|
list = list.filter((g) => (by === 'over' ? g.over : !g.over))
|
|
82
84
|
} else {
|
|
83
|
-
list = list.filter(
|
|
84
|
-
(
|
|
85
|
+
list = list.filter((g) => {
|
|
86
|
+
return (
|
|
85
87
|
g.id.endsWith(by) ||
|
|
86
88
|
Trax.names[g.rules].toLowerCase().includes(by) ||
|
|
87
89
|
g.p1.toLowerCase().includes(by) ||
|
|
88
90
|
g.p2.toLowerCase().includes(by) ||
|
|
89
|
-
g.moves.startsWith(by + ' ')
|
|
90
|
-
|
|
91
|
+
g.moves.startsWith(by + ' ') ||
|
|
92
|
+
g.note.includes(by)
|
|
93
|
+
)
|
|
94
|
+
})
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
|
|
@@ -96,6 +100,19 @@ const listSort = (list, filters) => {
|
|
|
96
100
|
return list
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
/** Find how many columns are left over after all the other columns arefilled.
|
|
104
|
+
* @arg {Record<string, number>} sizes
|
|
105
|
+
* @returns {number} number of columns left over
|
|
106
|
+
*/
|
|
107
|
+
const leftover = (sizes) => {
|
|
108
|
+
let left = process.stdout.columns
|
|
109
|
+
for (const size of Object.values(sizes)) {
|
|
110
|
+
left -= size
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return left
|
|
114
|
+
}
|
|
115
|
+
|
|
99
116
|
listCmd.fn = (CLI, ...filters) => {
|
|
100
117
|
if (Object.keys(CLI.GAMES).length === 0) {
|
|
101
118
|
return CLI.error('No games. Type "new" to start a new game.')
|
|
@@ -106,12 +123,14 @@ listCmd.fn = (CLI, ...filters) => {
|
|
|
106
123
|
|
|
107
124
|
let list = []
|
|
108
125
|
const size = {
|
|
126
|
+
spacing: 13, // Amount of space taken up by fixed-length and between columns
|
|
109
127
|
id: 1,
|
|
110
128
|
name: 1,
|
|
111
129
|
moves: 1,
|
|
112
130
|
p1: 1,
|
|
113
131
|
p2: 1,
|
|
114
132
|
}
|
|
133
|
+
let noteSize = Number.POSITIVE_INFINITY
|
|
115
134
|
|
|
116
135
|
for (const game of Object.values(CLI.GAMES)) {
|
|
117
136
|
const moves = game.moves.length === 0 ? 0 : game.moves.split(' ').length
|
|
@@ -120,13 +139,14 @@ listCmd.fn = (CLI, ...filters) => {
|
|
|
120
139
|
id: (game.id === CLI.GAME.id ? '* #' : '#') + String(game.id),
|
|
121
140
|
name: game.name?.length || 0,
|
|
122
141
|
rules: game.rules,
|
|
123
|
-
moves: CLI.plural(moves, 'move'),
|
|
142
|
+
moves: game.over ? 'game over' : CLI.plural(moves, 'move'),
|
|
124
143
|
smoves: game.over ? '9999' : String(moves).padStart(9, '0'),
|
|
125
144
|
sturn: String(game.turn),
|
|
126
145
|
p1: game.players?.[0] || 'white',
|
|
127
146
|
p2: game.players?.[1] || 'black',
|
|
128
147
|
turn: game.turn,
|
|
129
148
|
over: game.over,
|
|
149
|
+
note: game.notes?.[game.notes.length - 1]?.note || '',
|
|
130
150
|
})
|
|
131
151
|
}
|
|
132
152
|
|
|
@@ -138,6 +158,7 @@ listCmd.fn = (CLI, ...filters) => {
|
|
|
138
158
|
size.moves = Math.max(size.moves, game.moves.length)
|
|
139
159
|
size.p1 = Math.max(size.p1, game.p1.length)
|
|
140
160
|
size.p2 = Math.max(size.p2, game.p2.length)
|
|
161
|
+
noteSize = Math.min(noteSize, leftover(size))
|
|
141
162
|
}
|
|
142
163
|
|
|
143
164
|
for (const game of list) {
|
|
@@ -149,7 +170,8 @@ listCmd.fn = (CLI, ...filters) => {
|
|
|
149
170
|
CLI.color('vs '.padStart(size.p1 - game.p1.length + 4)) +
|
|
150
171
|
CLI.bubble(game.turn === 2 ? 'bh' : 'b', game.p2) +
|
|
151
172
|
CLI.color(' '.padStart(size.p2 - game.p2.length + 1)) +
|
|
152
|
-
CLI.color(game.
|
|
173
|
+
CLI.color(game.moves.padEnd(size.moves)) +
|
|
174
|
+
CLI.color.help(' ' + game.note.slice(0, noteSize)),
|
|
153
175
|
)
|
|
154
176
|
}
|
|
155
177
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Notes CLI command.
|
|
2
|
+
* @copyright 2022
|
|
3
|
+
* @author Chad Transtrum <chad@transtrum.net>
|
|
4
|
+
* @license Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const notesCmd = {
|
|
8
|
+
name: 'notes',
|
|
9
|
+
args: '[#id] <text>',
|
|
10
|
+
desc: 'add a note to a game',
|
|
11
|
+
help: [
|
|
12
|
+
'Add a short note to a game that will be shown when the game is',
|
|
13
|
+
'displayed, listed, or exported.',
|
|
14
|
+
],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
notesCmd.fn = (CLI, id, ...note) => {
|
|
18
|
+
const current = String(CLI.GAME.id)
|
|
19
|
+
if (!id) id = current
|
|
20
|
+
if (!/^#?\d+$/.test(id)) {
|
|
21
|
+
note.unshift(id)
|
|
22
|
+
id = current
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (id.startsWith('#')) id = id.slice(1)
|
|
26
|
+
|
|
27
|
+
const game = CLI.GAMES[id]
|
|
28
|
+
const notes = game.notes || []
|
|
29
|
+
const move = game.moves.length > 0 ? game.moves.split(' ').length : 0
|
|
30
|
+
|
|
31
|
+
notes.push({ move, note: note.join(' ') })
|
|
32
|
+
game.notes = notes
|
|
33
|
+
CLI.save()
|
|
34
|
+
}
|
package/src/cmds/play-try.js
CHANGED
package/src/cmds/view.js
CHANGED
|
@@ -39,7 +39,10 @@ viewCmd.fn = (CLI, move) => {
|
|
|
39
39
|
show = Number(move)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
const
|
|
42
|
+
const game = CLI.GAME
|
|
43
|
+
const players = game.players || ['white', 'black']
|
|
44
|
+
const puzzle = game.puzzle || ''
|
|
45
|
+
const { note } = [...(game.notes || [{}])].pop()
|
|
43
46
|
|
|
44
47
|
CLI.out(
|
|
45
48
|
CLI.color('#' + CLI.ID + ' ') +
|
|
@@ -50,4 +53,6 @@ viewCmd.fn = (CLI, move) => {
|
|
|
50
53
|
CLI.bubble('b' + (trax.turn === 2 ? 'h' : ''), players[1]),
|
|
51
54
|
)
|
|
52
55
|
CLI.display(trax, players, undefined, show)
|
|
56
|
+
if (puzzle) CLI.out(CLI.color('Puzzle ' + puzzle))
|
|
57
|
+
if (note) CLI.out(CLI.color.help(note))
|
|
53
58
|
}
|
package/src/engine.js
CHANGED
|
@@ -617,7 +617,7 @@ export const Trax = class {
|
|
|
617
617
|
return this.notation
|
|
618
618
|
.replace(/\n/g, ' ')
|
|
619
619
|
.split(' ')
|
|
620
|
-
.filter((n) => !n.endsWith('.'))
|
|
620
|
+
.filter((n) => Boolean(n) && !n.endsWith('.'))
|
|
621
621
|
}
|
|
622
622
|
|
|
623
623
|
/** Drop a tile onto the board. This is a lower level call. Use play() if possible instead.
|
package/src/threats.js
CHANGED
|
@@ -198,4 +198,5 @@ for (const one of Object.keys(corners)) {
|
|
|
198
198
|
threats.add(3, 'ralbmrW') // Attack into a wide cave
|
|
199
199
|
threats.add(3, 'Arlbmwr') // Reversed
|
|
200
200
|
|
|
201
|
+
threats.add(4, 'rb¡c?w!br') // Connectable pair on an edge
|
|
201
202
|
threats.add(4, 'ArlbmrW') // Wide cave ready for an attack
|
package/src/types.d.ts
CHANGED
|
@@ -62,6 +62,15 @@ type FoundThreats = Record<string, FoundThreat[]>
|
|
|
62
62
|
/** All the threats found for both the white and black player. */
|
|
63
63
|
type FoundThreatsCollection = { b: ColorThreats; w: ColorThreats }
|
|
64
64
|
|
|
65
|
+
/** A note with a move number */
|
|
66
|
+
type GameNote = {
|
|
67
|
+
move: number
|
|
68
|
+
note: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Notes saved in the game object in the CLI. */
|
|
72
|
+
type GameNotes = GameNote[]
|
|
73
|
+
|
|
65
74
|
/** A point-like object has numerical x and y properties. */
|
|
66
75
|
type PointLike = {
|
|
67
76
|
x: number
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '0.
|
|
2
|
+
export const version = '0.17.0'
|