@slugbugblue/trax-cli 0.11.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.
@@ -0,0 +1,177 @@
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.
14
+ */
15
+
16
+ // CLI list command
17
+
18
+ import process from 'node:process'
19
+
20
+ import { Trax } from '@slugbugblue/trax'
21
+
22
+ export const listCmd = {
23
+ name: 'list',
24
+ alt: 'ls',
25
+ args: '[sort|filter...] ["reverse"]',
26
+ desc: 'list all games',
27
+ comp:
28
+ 'id|number game|rules|name player1|p1 player2|p2 move|status turn' +
29
+ ' active|over trax|8x8|loop reverse',
30
+ help: [
31
+ 'Valid sort options are:',
32
+ ' "id": sort by game id',
33
+ ' "game": sort by game type',
34
+ ' "player1" or "player2": sort by player name',
35
+ ' "moves": sort by the number of moves',
36
+ 'Available filters:',
37
+ ' #id: match a game or a certain number of moves',
38
+ ' "active" or "over": whether a game is in progress or not',
39
+ ' "loop" or "8x8" or "trax": filter by game type',
40
+ " <other text>: match a player's name or the latest game note",
41
+ 'Add the "reverse" option to reverse the sort order.',
42
+ ],
43
+ }
44
+
45
+ const alphaSort = (a, b) => {
46
+ a = String(a).toLowerCase()
47
+ b = String(b).toLowerCase()
48
+ if (a < b) return -1
49
+ if (b < a) return 1
50
+ return 0
51
+ }
52
+
53
+ const sortBy = (sort) => {
54
+ if (!sort) return 'id'
55
+ sort = sort.toLowerCase()
56
+ if (sort === '#') return 'sid'
57
+ if ('ids'.startsWith(sort)) return 'sid'
58
+ if ('games'.startsWith(sort)) return 'rules'
59
+ if ('rules'.startsWith(sort)) return 'rules'
60
+ if ('names'.startsWith(sort)) return 'rules'
61
+ if ('numbers'.startsWith(sort)) return 'sid'
62
+ if ('moves'.startsWith(sort)) return 'smoves'
63
+ if ('status'.startsWith(sort)) return 'smoves'
64
+ if ('turn'.startsWith(sort)) return 'sturn'
65
+ if ('player'.startsWith(sort) && sort.endsWith('1')) return 'p1'
66
+ if ('player'.startsWith(sort) && sort.endsWith('2')) return 'p2'
67
+ if ('reverse'.startsWith(sort)) return 'reverse'
68
+ return sort
69
+ }
70
+
71
+ const listSort = (list, filters) => {
72
+ if (list.length === 0) return list
73
+ let reverse = false
74
+ for (const sort of filters) {
75
+ const by = sortBy(sort)
76
+ if (by === 'reverse') {
77
+ reverse = true
78
+ } else if (list[0]?.[by]) {
79
+ list.sort((a, b) => alphaSort(a[by], b[by]))
80
+ } else if (by === 'trax') {
81
+ list = list.filter((g) => g.rules === by)
82
+ } else if (['over', 'active'].includes(by)) {
83
+ list = list.filter((g) => (by === 'over' ? g.over : !g.over))
84
+ } else {
85
+ list = list.filter((g) => {
86
+ return (
87
+ g.id.endsWith(by) ||
88
+ Trax.names[g.rules].toLowerCase().includes(by) ||
89
+ g.p1.toLowerCase().includes(by) ||
90
+ g.p2.toLowerCase().includes(by) ||
91
+ g.moves.startsWith(by + ' ') ||
92
+ g.note.includes(by)
93
+ )
94
+ })
95
+ }
96
+ }
97
+
98
+ if (reverse) list.reverse()
99
+
100
+ return list
101
+ }
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
+
116
+ listCmd.fn = (CLI, ...filters) => {
117
+ if (Object.keys(CLI.GAMES).length === 0) {
118
+ return CLI.error('No games. Type "new" to start a new game.')
119
+ }
120
+
121
+ const puzzle = filters.find((f) => f.length > 1 && 'puzzles'.startsWith(f))
122
+ if (puzzle) return CLI.do('puzzles', ...filters.filter((f) => f !== puzzle))
123
+
124
+ let list = []
125
+ const size = {
126
+ spacing: 13, // Amount of space taken up by fixed-length and between columns
127
+ id: 1,
128
+ name: 1,
129
+ moves: 1,
130
+ p1: 1,
131
+ p2: 1,
132
+ }
133
+ let noteSize = Number.POSITIVE_INFINITY
134
+
135
+ for (const game of Object.values(CLI.GAMES)) {
136
+ const moves = game.moves.length === 0 ? 0 : game.moves.split(' ').length
137
+ list.push({
138
+ sid: String(game.id).padStart(9, '0'),
139
+ id: (game.id === CLI.GAME.id ? '* #' : '#') + String(game.id),
140
+ name: game.name?.length || 0,
141
+ rules: game.rules,
142
+ moves: game.over ? 'game over' : CLI.plural(moves, 'move'),
143
+ smoves: game.over ? '9999' : String(moves).padStart(9, '0'),
144
+ sturn: String(game.turn),
145
+ p1: game.players?.[0] || 'white',
146
+ p2: game.players?.[1] || 'black',
147
+ turn: game.turn,
148
+ over: game.over,
149
+ note: game.notes?.[game.notes.length - 1]?.note || '',
150
+ })
151
+ }
152
+
153
+ list = listSort(list, filters)
154
+
155
+ for (const game of list) {
156
+ size.id = Math.max(size.id, game.id.length)
157
+ size.name = Math.max(size.name, game.name)
158
+ size.moves = Math.max(size.moves, game.moves.length)
159
+ size.p1 = Math.max(size.p1, game.p1.length)
160
+ size.p2 = Math.max(size.p2, game.p2.length)
161
+ noteSize = Math.min(noteSize, leftover(size))
162
+ }
163
+
164
+ for (const game of list) {
165
+ CLI.out(
166
+ CLI.color(game.id.padStart(size.id) + ' ') +
167
+ Trax.names[game.rules] +
168
+ ' '.repeat(size.name - game.name + 1) +
169
+ CLI.bubble(game.turn === 1 ? 'wh' : 'w', game.p1) +
170
+ CLI.color('vs '.padStart(size.p1 - game.p1.length + 4)) +
171
+ CLI.bubble(game.turn === 2 ? 'bh' : 'b', game.p2) +
172
+ CLI.color(' '.padStart(size.p2 - game.p2.length + 1)) +
173
+ CLI.color(game.moves.padEnd(size.moves)) +
174
+ CLI.color.help(' ' + game.note.slice(0, noteSize)),
175
+ )
176
+ }
177
+ }
@@ -0,0 +1,136 @@
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.
14
+ */
15
+
16
+ // CLI new command
17
+
18
+ export const newCmd = {
19
+ name: 'new',
20
+ alt: 'start',
21
+ args: '[variant] [[p1 name] "vs" [p2 name]]',
22
+ comp: 'trax|loop|8x8 vs',
23
+ desc: 'start a new game',
24
+ help: [
25
+ 'Valid variants are "trax", "loop", and "8x8". Defaults to "trax" if not',
26
+ 'specified. Use the "vs" keyword to use specific names for the players.',
27
+ ],
28
+ }
29
+
30
+ // Okay, yes, this is silly
31
+ const pairs = [
32
+ ['Abbott', 'Costello'],
33
+ ['Adam', 'Eve'],
34
+ ['Anne Boleyn', 'Henry VIII'],
35
+ ['Ash', 'Pikachu'],
36
+ ['Barbie', 'Ken'],
37
+ ['Batman', 'Robin'],
38
+ ['Bert', 'Ernie'],
39
+ ['Bonnie', 'Clyde'],
40
+ ['Calvin', 'Hobbes'],
41
+ ['Chip', 'Dale'],
42
+ ['Donald', 'Daisy'],
43
+ ['Fred', 'Barney'],
44
+ ['Fred', 'George'],
45
+ ['Frodo', 'Samwise'],
46
+ ['Han', 'Chewie'],
47
+ ['Harry', 'Ron'],
48
+ ['Holmes', 'Watson'],
49
+ ['Jekyll', 'Hyde'],
50
+ ['Jim', 'Pam'],
51
+ ['Joe', 'Volcano'],
52
+ ['John', 'Yoko'],
53
+ ['King Kong', 'Godzilla'],
54
+ ['Lewis', 'Clark'],
55
+ ['Lilo', 'Stitch'],
56
+ ['Luke', 'Leia'],
57
+ ['Marge', 'Homer'],
58
+ ['Mario', 'Luigi'],
59
+ ['Marlin', 'Dory'],
60
+ ['Mary-Kate', 'Ashley'],
61
+ ['Mickey', 'Minnie'],
62
+ ['Miss Piggy', 'Kermit'],
63
+ ['Mork', 'Mindy'],
64
+ ['Obi-Wan', 'Anakin'],
65
+ ['Pan', 'Hook'],
66
+ ['Phineas', 'Ferb'],
67
+ ['player 1', 'player 2'],
68
+ ['p1', 'p2'],
69
+ ['R2-D2', 'C-3PO'],
70
+ ['Ren', 'Stimpy'],
71
+ ['Rick', 'Morty'],
72
+ ['Romeo', 'Juliet'],
73
+ ['Shaggy', 'Scooby'],
74
+ ['Shrek', 'Fiona'],
75
+ ['Simon', 'Garfunkel'],
76
+ ['Snoopy', 'Woodstock'],
77
+ ['Sonny', 'Cher'],
78
+ ['Spongebob', 'Patrick'],
79
+ ['Tarzan', 'Jane'],
80
+ ['Thelma', 'Louise'],
81
+ ['Thing 1', 'Thing 2'],
82
+ ['Tom', 'Huckleberry'],
83
+ ['Tom', 'Jerry'],
84
+ ['Tweety', 'Sylvester'],
85
+ ['Waldorf', 'Statler'],
86
+ ['Wallace', 'Gromit'],
87
+ ['white', 'black'],
88
+ ['Woody', 'Buzz'],
89
+ ]
90
+
91
+ const pairUp = () => {
92
+ const duo = pairs[Math.floor(Math.random() * pairs.length)]
93
+ if (duo[0].toLowerCase() !== duo[0] && Math.random() < 0.5) duo.reverse()
94
+ return duo
95
+ }
96
+
97
+ newCmd.fn = (CLI, variant, ...names) => {
98
+ let rules = ''
99
+ if (variant) {
100
+ variant = variant.toLowerCase()
101
+ if (variant.length > 1 && 'puzzles'.startsWith(variant)) {
102
+ return CLI.do('puzzles', 'new', ...names)
103
+ }
104
+
105
+ if (['8x8', '8', 'trax8x8', '8trax', '8x8trax'].includes(variant)) {
106
+ rules = 'trax8'
107
+ }
108
+
109
+ if (['loop', 'l', 'looptrax'].includes(variant)) rules = 'traxloop'
110
+ if (variant === 't') rules = 'trax'
111
+ if (['trax', 'traxloop', 'trax8'].includes(variant)) rules = variant
112
+
113
+ if (!rules) {
114
+ if (names.includes('vs') || names.includes('VS')) {
115
+ // We don't have a variant, we have player names
116
+ names.unshift(variant)
117
+ } else {
118
+ CLI.error('Unknown Trax variant.')
119
+ return CLI.do('help', 'new')
120
+ }
121
+ }
122
+ }
123
+
124
+ const players = pairUp()
125
+ if (names.length > 0) {
126
+ let vs = names.indexOf('vs')
127
+ if (vs === -1) vs = names.indexOf('VS')
128
+ if (vs >= 0) {
129
+ players[0] = names.slice(0, vs).join(' ') || 'white'
130
+ players[1] = names.slice(vs + 1).join(' ') || 'black'
131
+ }
132
+ }
133
+
134
+ const id = CLI.newGame(rules, players, '')
135
+ CLI.out(CLI.color('Started new game #' + id))
136
+ }
@@ -0,0 +1,37 @@
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
+
29
+ if (!game) return CLI.error('Game not found.')
30
+
31
+ const notes = game.notes || []
32
+ const move = game.moves.length > 0 ? game.moves.split(' ').length : 0
33
+
34
+ notes.push({ move, note: note.join(' ') })
35
+ game.notes = notes
36
+ CLI.save()
37
+ }
@@ -0,0 +1,175 @@
1
+ /* Copyright 2022-2023 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.
14
+ */
15
+
16
+ // CLI play/try commands
17
+
18
+ import { suggest } from '@slugbugblue/trax-analyst'
19
+ import { timeString } from '@slugbugblue/trax-cli/utils'
20
+
21
+ const moveRx = /^\d+\.?$/
22
+ const notationRx = /^[@a-z]+\d+[bps/\\+]$/i
23
+
24
+ export const playCmd = {
25
+ name: 'play',
26
+ alt: ['move', 'mv'],
27
+ args: '<move> [...]',
28
+ comp: '<play>',
29
+ desc: 'play a move',
30
+ rx: [moveRx, notationRx],
31
+ help: [
32
+ 'Enter the move in standard Trax notation. The final character of the',
33
+ 'notation can be replaced with a letter for ease of entering at the',
34
+ 'command line. Use <s> for "/", <b> for "\\", and <p> for "+".',
35
+ 'Multiple moves can be submitted at once. Move numbers are optional,',
36
+ 'but if included, they will be checked for accuracy.',
37
+ ],
38
+ }
39
+
40
+ export const tryCmd = {
41
+ name: 'try',
42
+ alt: 'see',
43
+ args: '<move>',
44
+ comp: '<play>',
45
+ desc: 'see the effects of a move',
46
+ help: [
47
+ 'View a move without committing to it. This is useful for seeing the',
48
+ 'effects of forced tiles.',
49
+ ],
50
+ }
51
+
52
+ const bot = (CLI) => {
53
+ const game = CLI.GAME
54
+ const trax = CLI.TRAX
55
+ const players = game.players || ['a', 'b']
56
+ const name = players[trax.turn - 1]
57
+ if (trax.over && game.puzzle) {
58
+ if (trax.move <= game.max && name !== 'puzzlebot') {
59
+ CLI.puzzleSolved(game.puzzle)
60
+ return 'win'
61
+ }
62
+
63
+ return 'lose'
64
+ }
65
+
66
+ if (!trax.over && /bot\b/i.test(name)) {
67
+ if (game.puzzle && trax.move >= game.max) {
68
+ trax.play('puzzled')
69
+ return 'lose'
70
+ }
71
+
72
+ CLI.do('view')
73
+ CLI.out(CLI.bubble(trax.color + 'd', `${name} thinking...`))
74
+ const start = Date.now()
75
+ const move = suggest(trax)?.pick?.move
76
+ const ms = Date.now() - start
77
+ if (move) {
78
+ CLI.out(
79
+ CLI.bubble(trax.color + 'd', `${name} chooses ${move}`) +
80
+ CLI.color.black(' in ' + timeString(ms)),
81
+ )
82
+ trax.play(move)
83
+ }
84
+
85
+ if (game.puzzle) return ' and win by move ' + String(game.max)
86
+ }
87
+
88
+ return ''
89
+ }
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
+ }