@slugbugblue/trax-cli 0.12.1 → 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/src/cmds/list.js CHANGED
@@ -1,19 +1,10 @@
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.
1
+ /** CLI list command
2
+ * @copyright 2022-2026
3
+ * @author Chad Transtrum <chad@transtrum.net>
4
+ * @license Apache-2.0
14
5
  */
15
6
 
16
- // CLI list command
7
+ /** @typedef {import('../cli.js').CLIContext} CLIContext */
17
8
 
18
9
  import process from 'node:process'
19
10
  import { Trax } from '@slugbugblue/trax'
@@ -39,8 +30,83 @@ export const listCmd = {
39
30
  " <other text>: match a player's name or the latest game note",
40
31
  'Add the "reverse" option to reverse the sort order.',
41
32
  ],
33
+ /** @param {CLIContext} CLI @param {...string} filters */
34
+ fn(CLI, ...filters) {
35
+ if (Object.keys(CLI.GAMES).length === 0) {
36
+ return CLI.error('No games. Type "new" to start a new game.')
37
+ }
38
+
39
+ const puzzle = filters.find((f) => f.length > 1 && 'puzzles'.startsWith(f))
40
+ if (puzzle) return CLI.do('puzzles', ...filters.filter((f) => f !== puzzle))
41
+
42
+ /** @type {Array<Record<string, string | number | boolean | undefined>>} */
43
+ let list = []
44
+ const size = {
45
+ spacing: 13, // Amount of space taken up by fixed-length and between columns
46
+ id: 1,
47
+ name: 1,
48
+ moves: 1,
49
+ p1: 1,
50
+ p2: 1,
51
+ }
52
+
53
+ for (const game of Object.values(CLI.GAMES)) {
54
+ const moves = game.moves.length === 0 ? 0 : game.moves.split(' ').length
55
+ list.push({
56
+ sid: String(game.id).padStart(9, '0'),
57
+ id: (game.id === CLI.GAME.id ? '* #' : '#') + String(game.id),
58
+ name: game.name?.length || 0,
59
+ rules: game.rules,
60
+ moves: game.over ? 'game over' : CLI.plural(moves, 'move'),
61
+ smoves: game.over ? '9999' : String(moves).padStart(9, '0'),
62
+ sturn: String(game.turn),
63
+ p1: game.players?.[0] || 'white',
64
+ p2: game.players?.[1] || 'black',
65
+ turn: game.turn,
66
+ over: game.over,
67
+ note: game.notes?.at(-1)?.note || '',
68
+ })
69
+ }
70
+
71
+ list = listSort(list, filters)
72
+ renderList(CLI, list, size)
73
+ },
74
+ }
75
+
76
+ /**
77
+ * @param {CLIContext} CLI - the CLI object
78
+ * @param {Array<Record<string, string | number | boolean | undefined>>} list - sorted game list
79
+ * @param {Record<string, number>} size - initial column width minimums
80
+ */
81
+ const renderList = (CLI, list, size) => {
82
+ let noteSize = Number.POSITIVE_INFINITY
83
+ for (const game of list) {
84
+ size.id = Math.max(size.id, String(game.id).length)
85
+ size.name = Math.max(size.name, Number(game.name))
86
+ size.moves = Math.max(size.moves, String(game.moves).length)
87
+ size.p1 = Math.max(size.p1, String(game.p1).length)
88
+ size.p2 = Math.max(size.p2, String(game.p2).length)
89
+ noteSize = Math.min(noteSize, leftover(size))
90
+ }
91
+
92
+ for (const game of list) {
93
+ CLI.out(
94
+ CLI.color(String(game.id).padStart(size.id) + ' ') +
95
+ Trax.names[
96
+ /** @type {import('../cli.js').TraxVariant} */ (String(game.rules))
97
+ ] +
98
+ ' '.repeat(size.name - Number(game.name) + 1) +
99
+ CLI.bubble(game.turn === 1 ? 'wh' : 'w', String(game.p1)) +
100
+ CLI.color('vs '.padStart(size.p1 - String(game.p1).length + 4)) +
101
+ CLI.bubble(game.turn === 2 ? 'bh' : 'b', String(game.p2)) +
102
+ CLI.color(' '.padStart(size.p2 - String(game.p2).length + 1)) +
103
+ CLI.color(String(game.moves).padEnd(size.moves)) +
104
+ CLI.color.help(' ' + String(game.note).slice(0, noteSize)),
105
+ )
106
+ }
42
107
  }
43
108
 
109
+ /** @param {string} a @param {string} b @returns {number} */
44
110
  const alphaSort = (a, b) => {
45
111
  a = String(a).toLowerCase()
46
112
  b = String(b).toLowerCase()
@@ -49,6 +115,7 @@ const alphaSort = (a, b) => {
49
115
  return 0
50
116
  }
51
117
 
118
+ /** @param {string | undefined} sort @returns {string} */
52
119
  const sortBy = (sort) => {
53
120
  if (!sort) return 'id'
54
121
  sort = sort.toLowerCase()
@@ -67,6 +134,11 @@ const sortBy = (sort) => {
67
134
  return sort
68
135
  }
69
136
 
137
+ /**
138
+ * @param {Array<Record<string, string | number | boolean | undefined>>} list
139
+ * @param {string[]} filters
140
+ * @returns {Array<Record<string, string | number | boolean | undefined>>}
141
+ */
70
142
  const listSort = (list, filters) => {
71
143
  if (list.length === 0) return list
72
144
  let reverse = false
@@ -75,7 +147,7 @@ const listSort = (list, filters) => {
75
147
  if (by === 'reverse') {
76
148
  reverse = true
77
149
  } else if (list[0]?.[by]) {
78
- list.sort((a, b) => alphaSort(a[by], b[by]))
150
+ list.sort((a, b) => alphaSort(String(a[by]), String(b[by])))
79
151
  } else if (by === 'trax') {
80
152
  list = list.filter((g) => g.rules === by)
81
153
  } else if (['over', 'active'].includes(by)) {
@@ -83,12 +155,16 @@ const listSort = (list, filters) => {
83
155
  } else {
84
156
  list = list.filter((g) => {
85
157
  return (
86
- g.id.endsWith(by) ||
87
- Trax.names[g.rules].toLowerCase().includes(by) ||
88
- g.p1.toLowerCase().includes(by) ||
89
- g.p2.toLowerCase().includes(by) ||
90
- g.moves.startsWith(by + ' ') ||
91
- g.note.includes(by)
158
+ String(g.id).endsWith(by) ||
159
+ Trax.names[
160
+ /** @type {import('../cli.js').TraxVariant} */ (String(g.rules))
161
+ ]
162
+ .toLowerCase()
163
+ .includes(by) ||
164
+ String(g.p1).toLowerCase().includes(by) ||
165
+ String(g.p2).toLowerCase().includes(by) ||
166
+ String(g.moves).startsWith(by + ' ') ||
167
+ String(g.note).includes(by)
92
168
  )
93
169
  })
94
170
  }
@@ -99,9 +175,9 @@ const listSort = (list, filters) => {
99
175
  return list
100
176
  }
101
177
 
102
- /** Find how many columns are left over after all the other columns arefilled.
103
- * @arg {Record<string, number>} sizes
104
- * @returns {number} number of columns left over
178
+ /** Find how many columns are left over after all the other columns are filled.
179
+ * @param {Record<string, number>} sizes
180
+ * @returns {number}
105
181
  */
106
182
  const leftover = (sizes) => {
107
183
  let left = process.stdout.columns
@@ -111,66 +187,3 @@ const leftover = (sizes) => {
111
187
 
112
188
  return left
113
189
  }
114
-
115
- listCmd.fn = (CLI, ...filters) => {
116
- if (Object.keys(CLI.GAMES).length === 0) {
117
- return CLI.error('No games. Type "new" to start a new game.')
118
- }
119
-
120
- const puzzle = filters.find((f) => f.length > 1 && 'puzzles'.startsWith(f))
121
- if (puzzle) return CLI.do('puzzles', ...filters.filter((f) => f !== puzzle))
122
-
123
- let list = []
124
- const size = {
125
- spacing: 13, // Amount of space taken up by fixed-length and between columns
126
- id: 1,
127
- name: 1,
128
- moves: 1,
129
- p1: 1,
130
- p2: 1,
131
- }
132
- let noteSize = Number.POSITIVE_INFINITY
133
-
134
- for (const game of Object.values(CLI.GAMES)) {
135
- const moves = game.moves.length === 0 ? 0 : game.moves.split(' ').length
136
- list.push({
137
- sid: String(game.id).padStart(9, '0'),
138
- id: (game.id === CLI.GAME.id ? '* #' : '#') + String(game.id),
139
- name: game.name?.length || 0,
140
- rules: game.rules,
141
- moves: game.over ? 'game over' : CLI.plural(moves, 'move'),
142
- smoves: game.over ? '9999' : String(moves).padStart(9, '0'),
143
- sturn: String(game.turn),
144
- p1: game.players?.[0] || 'white',
145
- p2: game.players?.[1] || 'black',
146
- turn: game.turn,
147
- over: game.over,
148
- note: game.notes?.[game.notes.length - 1]?.note || '',
149
- })
150
- }
151
-
152
- list = listSort(list, filters)
153
-
154
- for (const game of list) {
155
- size.id = Math.max(size.id, game.id.length)
156
- size.name = Math.max(size.name, game.name)
157
- size.moves = Math.max(size.moves, game.moves.length)
158
- size.p1 = Math.max(size.p1, game.p1.length)
159
- size.p2 = Math.max(size.p2, game.p2.length)
160
- noteSize = Math.min(noteSize, leftover(size))
161
- }
162
-
163
- for (const game of list) {
164
- CLI.out(
165
- CLI.color(game.id.padStart(size.id) + ' ') +
166
- Trax.names[game.rules] +
167
- ' '.repeat(size.name - game.name + 1) +
168
- CLI.bubble(game.turn === 1 ? 'wh' : 'w', game.p1) +
169
- CLI.color('vs '.padStart(size.p1 - game.p1.length + 4)) +
170
- CLI.bubble(game.turn === 2 ? 'bh' : 'b', game.p2) +
171
- CLI.color(' '.padStart(size.p2 - game.p2.length + 1)) +
172
- CLI.color(game.moves.padEnd(size.moves)) +
173
- CLI.color.help(' ' + game.note.slice(0, noteSize)),
174
- )
175
- }
176
- }
package/src/cmds/new.js CHANGED
@@ -1,19 +1,10 @@
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.
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
- // CLI new command
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
@@ -32,6 +68,7 @@ const pairs = [
32
68
  ['Aang', 'Katara'],
33
69
  ['Abbott', 'Costello'],
34
70
  ['Adam', 'Eve'],
71
+ ['Ali', 'Frazier'],
35
72
  ['Anne Boleyn', 'Henry VIII'],
36
73
  ['Ash', 'Pikachu'],
37
74
  ['Barbie', 'Ken'],
@@ -44,12 +81,15 @@ const pairs = [
44
81
  ['Chip', 'Dale'],
45
82
  ['Donald', 'Daisy'],
46
83
  ['Depp', 'Heard'],
84
+ ['Dorothy', 'Toto'],
47
85
  ['Fred', 'Barney'],
48
86
  ['Fred', 'George'],
87
+ ['Frida', 'Diego'],
49
88
  ['Frodo', 'Samwise'],
50
89
  ['Hamilton', 'Burr'],
51
90
  ['Han', 'Chewie'],
52
91
  ['Harry', 'Ron'],
92
+ ['Hatfield', 'McCoy'],
53
93
  ['Holmes', 'Moriarty'],
54
94
  ['Holmes', 'Watson'],
55
95
  ['Iñigo', 'Fezzik'],
@@ -73,6 +113,7 @@ const pairs = [
73
113
  ['Pan', 'Hook'],
74
114
  ['Phineas', 'Ferb'],
75
115
  ['player 1', 'player 2'],
116
+ ['p1', 'p2'],
76
117
  ['Potter', 'Voldemort'],
77
118
  ['R2-D2', 'C-3PO'],
78
119
  ['Ren', 'Stimpy'],
@@ -85,6 +126,7 @@ const pairs = [
85
126
  ['Snoopy', 'Woodstock'],
86
127
  ['Sonny', 'Cher'],
87
128
  ['Spongebob', 'Patrick'],
129
+ ['Sully', 'Neytiri'],
88
130
  ['Tarzan', 'Jane'],
89
131
  ['Thelma', 'Louise'],
90
132
  ['Thing 1', 'Thing 2'],
@@ -103,44 +145,3 @@ const pairUp = () => {
103
145
  if (duo[0].toLowerCase() !== duo[0] && Math.random() < 0.5) duo.reverse()
104
146
  return duo
105
147
  }
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
- notesCmd.fn = (CLI, id, ...note) => {
18
- const current = String(CLI.GAME.id)
19
- id ||= current
20
- if (!/^#?\d+$/.test(id)) {
21
- note.unshift(id)
22
- id = current
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
- if (id.startsWith('#')) id = id.slice(1)
26
+ if (id.startsWith('#')) id = id.slice(1)
26
27
 
27
- const game = CLI.GAMES[id]
28
+ const game = CLI.GAMES[id]
28
29
 
29
- if (!game) return CLI.error('Game not found.')
30
+ if (!game) return CLI.error('Game not found.')
30
31
 
31
- const notes = game.notes || []
32
- const move = game.moves.length > 0 ? game.moves.split(' ').length : 0
32
+ const notes = game.notes || []
33
+ const move = game.moves.length > 0 ? game.moves.split(' ').length : 0
33
34
 
34
- notes.push({ move, note: note.join(' ') })
35
- game.notes = notes
36
- CLI.save()
35
+ notes.push({ move, note: note.join(' ') })
36
+ game.notes = notes
37
+ CLI.save()
38
+ },
37
39
  }