@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slugbugblue/trax",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Trax game engine and friends",
5
5
  "keywords": [
6
6
  "trax",
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 += ' (' + source.url + ')'
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.notation + '\n'
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
- (g) =>
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.over ? 'game over' : game.moves),
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 players = CLI.GAME.players || ['white', 'black']
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.15.0'
2
+ export const version = '0.16.0'