@slugbugblue/trax 0.15.0 → 0.16.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/package.json +1 -1
- package/src/analyst.js +1 -1
- package/src/cli.js +29 -22
- package/src/cmds/import-export.js +60 -2
- package/src/cmds/list.js +29 -7
- package/src/cmds/notes.js +34 -0
- 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,15 @@
|
|
|
1
1
|
# @slugbugblue/trax
|
|
2
2
|
|
|
3
|
+
## 0.16.0 - 2022-12-27
|
|
4
|
+
|
|
5
|
+
- Added a `notes` command to the CLI to add comments to a game
|
|
6
|
+
|
|
7
|
+
- can be used as a text filter by the `list` command
|
|
8
|
+
- will be displayed by the `view` command
|
|
9
|
+
- will be included in the output generated by the `export` command
|
|
10
|
+
|
|
11
|
+
- Minor analysis improvements
|
|
12
|
+
|
|
3
13
|
## 0.15.0 - 2022-12-14
|
|
4
14
|
|
|
5
15
|
- Added shared `puzzles` commands to the CLI:
|
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,
|
|
@@ -188,6 +188,63 @@ importCmd.fn = async (CLI, filename) => {
|
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/** Get all the notes for a certain move.
|
|
192
|
+
* @arg {number} move - The move number
|
|
193
|
+
* @arg {GameNotes} notes? - The notes for a game
|
|
194
|
+
* @returns {string} - Notes formatted for this move, or empty string if none
|
|
195
|
+
*/
|
|
196
|
+
const gameNotes = (move, notes) => {
|
|
197
|
+
notes = notes || []
|
|
198
|
+
const moveNotes = notes.filter((n) => n.move === move)
|
|
199
|
+
if (moveNotes.length === 0) return ''
|
|
200
|
+
return (
|
|
201
|
+
'; ' +
|
|
202
|
+
moveNotes.map((n) => n.note.replaceAll('\n', '\n; ')).join('\n; ') +
|
|
203
|
+
'\n'
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Interleave the game moves with any notes, formatted for the export file.
|
|
208
|
+
* @arg {string[]} moves - an array of move notations
|
|
209
|
+
* @arg {GameNotes} notes? - The notes for a game
|
|
210
|
+
* @returns {string} - A string that can be placed into an export file
|
|
211
|
+
*/
|
|
212
|
+
const interleaveNotation = (moves, notes) => {
|
|
213
|
+
let content = ''
|
|
214
|
+
let moveNumber = 0
|
|
215
|
+
let annotation = ''
|
|
216
|
+
for (const move of moves) {
|
|
217
|
+
const comments = gameNotes(moveNumber, notes)
|
|
218
|
+
if (comments) {
|
|
219
|
+
const [lengthA, lengthC] = [annotation.length, comments.length]
|
|
220
|
+
const space = lengthA > 0 ? (lengthA + lengthC < 78 ? ' ' : '\n') : ''
|
|
221
|
+
content += space + annotation + comments
|
|
222
|
+
annotation = ''
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
moveNumber += 1
|
|
226
|
+
const notation = String(moveNumber) + '. ' + move
|
|
227
|
+
if (annotation.length + notation.length < 79) {
|
|
228
|
+
annotation += (annotation.length > 0 ? ' ' : '') + notation
|
|
229
|
+
} else {
|
|
230
|
+
content += '\n' + annotation
|
|
231
|
+
annotation = notation
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const comments = gameNotes(moveNumber, notes)
|
|
236
|
+
if (comments) {
|
|
237
|
+
const [lengthA, lengthC] = [annotation.length, comments.length]
|
|
238
|
+
const space = lengthA > 0 ? (lengthA + lengthC < 78 ? ' ' : '\n') : ''
|
|
239
|
+
content += space + annotation + comments
|
|
240
|
+
annotation = ''
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
content += annotation
|
|
244
|
+
if (!content.endsWith('\n')) content += '\n'
|
|
245
|
+
return content
|
|
246
|
+
}
|
|
247
|
+
|
|
191
248
|
exportCmd.fn = (CLI, id, filename) => {
|
|
192
249
|
cli = CLI
|
|
193
250
|
let game = CLI.GAME
|
|
@@ -218,7 +275,7 @@ exportCmd.fn = (CLI, id, filename) => {
|
|
|
218
275
|
content += '; Puzzle ' + game.puzzle
|
|
219
276
|
if (source) {
|
|
220
277
|
content += ' provided courtesy of ' + source.name
|
|
221
|
-
if (source.url) content += '
|
|
278
|
+
if (source.url) content += '\n; ' + source.url
|
|
222
279
|
}
|
|
223
280
|
|
|
224
281
|
if (puzzle.title) content += '\n; ' + puzzle.title
|
|
@@ -230,7 +287,8 @@ exportCmd.fn = (CLI, id, filename) => {
|
|
|
230
287
|
}
|
|
231
288
|
}
|
|
232
289
|
|
|
233
|
-
content += trax.
|
|
290
|
+
content += interleaveNotation(trax.moves, game.notes)
|
|
291
|
+
|
|
234
292
|
if (filename) filename = filename.replace(/[^ a-z\d.~/]/g, '')
|
|
235
293
|
if (filename) {
|
|
236
294
|
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/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.16.0'
|