@slugbugblue/trax 0.8.0 → 0.9.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,71 @@
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 delete command
17
+
18
+ export const deleteCmd = {
19
+ name: 'delete',
20
+ alt: ['remove', 'rm'],
21
+ args: '[#id] ["force"]',
22
+ comp: '<id> force',
23
+ desc: 'delete a game',
24
+ help: [
25
+ 'Delete the current game. Or specify a game number to delete that game.',
26
+ 'If the game is in progress, you must type "force" to delete it.',
27
+ ],
28
+ }
29
+
30
+ const selectAnyGame = (CLI) => {
31
+ if (CLI.GAMES) {
32
+ const games = Object.values(CLI.GAMES).sort((a, b) => {
33
+ // Active games first
34
+ if (a.over && !b.over) return 1
35
+ if (b.over && !a.over) return -1
36
+ // Games with more moves first
37
+ return (b.moves?.length || 0) - (a.moves?.length || 0)
38
+ })
39
+ if (games.length > 0) {
40
+ CLI.do('select', String(games[0].id))
41
+ }
42
+ }
43
+ }
44
+
45
+ deleteCmd.fn = (CLI, id, force = 'x') => {
46
+ const current = String(CLI.GAME.id)
47
+ if (!id) id = current
48
+ if ('force'.startsWith(id)) {
49
+ id = current
50
+ force = 'force'
51
+ }
52
+
53
+ if (id.startsWith('#')) id = id.slice(1)
54
+
55
+ const game = CLI.GAMES[id]
56
+
57
+ if (!game || !String(game.id)) {
58
+ return CLI.error('Invalid id. Type "list" to see available games.')
59
+ }
60
+
61
+ if (game.over || game.moves === '' || 'force'.startsWith(force)) {
62
+ CLI.delete(id)
63
+ CLI.out(CLI.color('Deleted game #' + id + '.'))
64
+
65
+ if (id === String(current)) {
66
+ selectAnyGame(CLI)
67
+ }
68
+ } else {
69
+ CLI.error('Game #' + id + ' is still active. Use "force" to delete it.')
70
+ }
71
+ }
@@ -0,0 +1,81 @@
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 help command
17
+
18
+ export const helpCmd = {
19
+ name: 'help',
20
+ opts: ['--help', '-h', '-?'],
21
+ alt: '?',
22
+ args: '[topic]',
23
+ comp: '<cmd>',
24
+ desc: 'display information on commands',
25
+ help: [
26
+ 'Type "help <command>" or "<command> help" to see additional information',
27
+ 'for a specific command.',
28
+ ],
29
+ }
30
+
31
+ // Find the length of the printable characters of the string
32
+ const length = (text) => text.replace(/[<">]/g, '').length
33
+
34
+ helpCmd.fn = (CLI, word) => {
35
+ // If we are looking for help on a particular commands, get that command
36
+ word = CLI.resolveCommand(word) || (word ? String(word).toLowerCase() : '')
37
+ let cmds = CLI.commands.filter((c) => !word || c.startsWith(word))
38
+ if (cmds.length === 0) {
39
+ cmds = CLI.commands.filter((c) => c.includes(word))
40
+ }
41
+
42
+ // If we don't have a few commands, select all commands
43
+ if (cmds.length === 0) cmds = CLI.commands
44
+
45
+ // Load all the commands we need to print into a local variable
46
+ // and also use the loop to get the length of the longest args
47
+ let max = 1
48
+ const commands = {}
49
+ for (const key of cmds) {
50
+ commands[key] = CLI.cmd(key)
51
+ max = Math.max(max, key.length + length(commands[key].args))
52
+ }
53
+
54
+ // Print a summary of all matching commands
55
+ for (const key of cmds.sort()) {
56
+ const topic = commands[key]
57
+ if (topic.hide && !CLI.repl) continue // Hidden commands only in repl
58
+ const space = ' '.repeat(5 + max - length(topic.args) - key.length)
59
+ CLI.out(
60
+ CLI.color.command(` ${key} ${topic.args}`) +
61
+ CLI.color.short(space + topic.desc),
62
+ )
63
+ }
64
+
65
+ // And if we have just one command, print details on that command
66
+ if (cmds.length === 1) {
67
+ const topic = commands[cmds[0]]
68
+ for (const alias of topic.alt) {
69
+ CLI.out(CLI.color.command(` ${alias}`))
70
+ }
71
+
72
+ CLI.out('')
73
+ if (CLI.is(topic.help, 'str')) {
74
+ CLI.out(' ' + CLI.color.help(topic.help))
75
+ } else {
76
+ for (const line of topic.help) {
77
+ CLI.out(' ' + CLI.color.help(line))
78
+ }
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,250 @@
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 import/export commands
17
+
18
+ import fs from 'node:fs/promises'
19
+ import process from 'node:process'
20
+
21
+ import envPaths from 'env-paths'
22
+ import makeDir from 'make-dir'
23
+
24
+ import { Trax } from '../engine.js'
25
+
26
+ const paths = envPaths('trax', { suffix: '' })
27
+ const PATHS = {}
28
+
29
+ // Store a copy of the CLI context object locally
30
+ let cli = {}
31
+
32
+ const fsOuch = (fsError) => {
33
+ cli.out(fsError, cli.COLORS.fatal)
34
+ throw fsError
35
+ }
36
+
37
+ const expandPath = (name) => {
38
+ // Expand home directory ... anything else?
39
+ if (name.includes('~')) name = name.replace('~', process.env.HOME)
40
+ return name
41
+ }
42
+
43
+ // Return the actual path on disk, creating it if necessary.
44
+ // Path is one of cache, config, data, log, temp.
45
+ const getPath = async (path) => {
46
+ if (path in PATHS) return PATHS[path]
47
+ if (path in paths) {
48
+ const realPath = await makeDir(paths[path])
49
+ PATHS[path] = realPath
50
+ return realPath
51
+ }
52
+
53
+ return false
54
+ }
55
+
56
+ const hexy = (length = 6) => {
57
+ let hex = ''
58
+ while (hex.length < length) {
59
+ hex += '0123456789abcdef'[Math.floor(Math.random() * 16)]
60
+ }
61
+
62
+ return hex
63
+ }
64
+
65
+ export const getFile = async (filetype, filename, variable, decode) => {
66
+ const path = await getPath(filetype)
67
+ if (path) {
68
+ try {
69
+ const contents = await fs.readFile(`${path}/${filename}`, 'utf8')
70
+ for (const [key, value] of Object.entries(decode(contents))) {
71
+ variable[key] = value
72
+ }
73
+ } catch (fsError) {
74
+ if (fsError instanceof TypeError || fsError instanceof SyntaxError) {
75
+ cli.error(`Unable to read ${filetype} file ${path}/${filename}`)
76
+ } else if (fsError.code !== 'ENOENT') {
77
+ fsOuch(fsError)
78
+ }
79
+ }
80
+ }
81
+
82
+ return variable
83
+ }
84
+
85
+ export const saveFile = (filetype, filename, data) => {
86
+ getPath(filetype).then((path) => {
87
+ if (path) {
88
+ const parts = filename.split('.')
89
+ parts.splice(1, 0, hexy())
90
+ const tmpfile = path + '/' + parts.join('.')
91
+ filename = path + '/' + filename
92
+ fs.writeFile(tmpfile, data)
93
+ .then(() => {
94
+ fs.rm(filename, { force: true, maxRetries: 5 })
95
+ .then(() => {
96
+ fs.rename(tmpfile, filename).catch(fsOuch)
97
+ })
98
+ .catch(fsOuch)
99
+ })
100
+ .catch(fsOuch)
101
+ }
102
+ })
103
+ }
104
+
105
+ export const importCmd = {
106
+ name: 'import',
107
+ alt: ['load', 'open'],
108
+ args: '<filename>',
109
+ comp: '<file>',
110
+ desc: 'open a game file',
111
+ help: [
112
+ 'Load a ".trx" game file into memory. Once imported, it will be assigned',
113
+ 'a game id and automatically selected for future commands.',
114
+ ],
115
+ }
116
+
117
+ export const exportCmd = {
118
+ name: 'export',
119
+ alt: ['save', 'keep', 'share'],
120
+ args: '[#id] [filename]',
121
+ comp: '<id> <file>',
122
+ desc: 'save a game to a file',
123
+ help: [
124
+ 'Export the current game to an external ".trx" file for safe-keeping or',
125
+ 'to share. Specify a game id to export a different game.',
126
+ ],
127
+ }
128
+
129
+ importCmd.fn = async (CLI, filename) => {
130
+ cli = CLI
131
+ if (!filename) {
132
+ CLI.error('Missing filename.')
133
+ return CLI.do('help', 'import')
134
+ }
135
+
136
+ let content
137
+ try {
138
+ content = await fs.readFile(expandPath(filename), 'utf8')
139
+ } catch (fsError) {
140
+ if (fsError.code === 'ENOENT' && !filename.endsWith('.trx')) {
141
+ try {
142
+ content = await fs.readFile(expandPath(filename + '.trx'), 'utf8')
143
+ } catch {
144
+ content = null
145
+ }
146
+ }
147
+
148
+ if (!content) CLI.error(fsError.toString())
149
+ }
150
+
151
+ if (content) {
152
+ let game
153
+ let players = []
154
+ const moves = []
155
+ for (const line of content.split('\n')) {
156
+ if (!line) continue
157
+ const ln = line.split(/[#;]/)[0]
158
+ if (!ln) continue
159
+ const lower = ln.toLowerCase()
160
+ if (!game && lower.includes('trax')) {
161
+ game = 'trax'
162
+ if (lower.includes('loop')) game = 'traxloop'
163
+ if (lower.includes('8x8')) game = 'trax8'
164
+ } else if (players.length === 0 && lower.includes(' vs ')) {
165
+ players = ln.split(' ').filter((p) => p)
166
+ } else {
167
+ moves.push(...ln.split(' '))
168
+ }
169
+ }
170
+
171
+ if (game) {
172
+ CLI.do('new', game, ...players)
173
+ CLI.do('play', ...moves.filter((m) => m))
174
+ } else {
175
+ CLI.error(filename + ' does not appear to be formatted correctly.')
176
+ }
177
+ }
178
+ }
179
+
180
+ exportCmd.fn = (CLI, id, filename) => {
181
+ cli = CLI
182
+ let game = CLI.GAME
183
+ if (id) {
184
+ const newid = id.replace(/^#/, '')
185
+ game = CLI.GAMES[newid]
186
+ if (!game) {
187
+ filename = id
188
+ game = CLI.GAME
189
+ }
190
+ }
191
+
192
+ if (!game || !game.id) {
193
+ return CLI.error('No active game. Type "new" to start a game.')
194
+ }
195
+
196
+ const trax = new Trax(game.rules, game.moves, 'cli')
197
+
198
+ let content = Trax.names[trax.rules].replace(' ', '') + '\n'
199
+ content += game.players?.[0] || 'white'
200
+ content += ' vs '
201
+ content += game.players?.[1] || 'black'
202
+ content += '\n; @slugbugblue/trax cli.js v' + CLI.version + '\n'
203
+ content += trax.notation + '\n'
204
+ if (filename) filename = filename.replace(/[^ a-z\d.~/]/g, '')
205
+ if (filename) {
206
+ const fname = filename.split('/').pop()
207
+ if (!fname) filename += game.rules + game.id
208
+ if (!fname.includes('.')) filename += '.trx'
209
+ CLI.out(CLI.color('Exporting #' + game.id + ' to ') + filename)
210
+ fs.writeFile(expandPath(filename), content).catch(fsOuch)
211
+ } else {
212
+ CLI.out(content)
213
+ }
214
+ }
215
+
216
+ const FOLDERS = {}
217
+
218
+ export const findFiles = (path) => {
219
+ if (typeof path !== 'string') path = ''
220
+ let folder = '.'
221
+ if (path.includes('/')) {
222
+ folder = path.slice(0, path.lastIndexOf('/'))
223
+ }
224
+
225
+ const prefix = folder + '/'
226
+
227
+ folder = expandPath(folder)
228
+
229
+ if (!FOLDERS[prefix]) {
230
+ FOLDERS[prefix] = []
231
+ fs.readdir(folder, { withFileTypes: true })
232
+ .then((dir) => {
233
+ // This is all happening asynchronously, so we have to call findFiles
234
+ // at least twice before we get any useful information ...
235
+ const entries = []
236
+ for (const entry of dir) {
237
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
238
+ entries.push(prefix + entry.name + '/')
239
+ } else if (entry.isFile() && entry.name.endsWith('.trx')) {
240
+ entries.push(prefix + entry.name)
241
+ }
242
+ }
243
+
244
+ FOLDERS[prefix] = entries
245
+ })
246
+ .catch(() => {})
247
+ }
248
+
249
+ return FOLDERS[prefix]
250
+ }
@@ -0,0 +1,155 @@
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 { Trax } from '../engine.js'
19
+
20
+ export const listCmd = {
21
+ name: 'list',
22
+ alt: 'ls',
23
+ args: '[sort|filter...] ["reverse"]',
24
+ desc: 'list all games',
25
+ comp:
26
+ 'id|number game|rules|name player1|p1 player2|p2 move|status turn' +
27
+ ' active|over trax|8x8|loop reverse',
28
+ help: [
29
+ 'Valid sort options are:',
30
+ ' "id": sort by game id',
31
+ ' "game": sort by game type',
32
+ ' "player1" or "player2": sort by player name',
33
+ ' "moves": sort by the number of moves',
34
+ 'Available filters:',
35
+ ' #id: match a game or a certain number of moves',
36
+ ' "active" or "over": whether a game is in progress or not',
37
+ ' "loop" or "8x8" or "trax": filter by game type',
38
+ " <other text>: match a player's name",
39
+ 'Add the "reverse" option to reverse the sort order.',
40
+ ],
41
+ }
42
+
43
+ const plural = (n, noun, nouns) =>
44
+ n + ' ' + (Math.abs(n) === 1 ? noun : nouns ? nouns : noun + 's')
45
+
46
+ const alphaSort = (a, b) => {
47
+ a = String(a).toLowerCase()
48
+ b = String(b).toLowerCase()
49
+ if (a < b) return -1
50
+ if (b < a) return 1
51
+ return 0
52
+ }
53
+
54
+ const sortBy = (sort) => {
55
+ if (!sort) return 'id'
56
+ sort = sort.toLowerCase()
57
+ if (sort === '#') return 'sid'
58
+ if ('ids'.startsWith(sort)) return 'sid'
59
+ if ('games'.startsWith(sort)) return 'rules'
60
+ if ('rules'.startsWith(sort)) return 'rules'
61
+ if ('names'.startsWith(sort)) return 'rules'
62
+ if ('numbers'.startsWith(sort)) return 'sid'
63
+ if ('moves'.startsWith(sort)) return 'smoves'
64
+ if ('status'.startsWith(sort)) return 'smoves'
65
+ if ('turn'.startsWith(sort)) return 'sturn'
66
+ if ('player'.startsWith(sort) && sort.endsWith('1')) return 'p1'
67
+ if ('player'.startsWith(sort) && sort.endsWith('2')) return 'p2'
68
+ if ('reverse'.startsWith(sort)) return 'reverse'
69
+ return sort
70
+ }
71
+
72
+ const listSort = (list, filters) => {
73
+ if (list.length === 0) return list
74
+ let reverse = false
75
+ for (const sort of filters) {
76
+ const by = sortBy(sort)
77
+ if (by === 'reverse') {
78
+ reverse = true
79
+ } else if (list[0]?.[by]) {
80
+ list.sort((a, b) => alphaSort(a[by], b[by]))
81
+ } else if (by === 'trax') {
82
+ list = list.filter((g) => g.rules === by)
83
+ } else if (['over', 'active'].includes(by)) {
84
+ list = list.filter((g) => (by === 'over' ? g.over : !g.over))
85
+ } else {
86
+ list = list.filter(
87
+ (g) =>
88
+ g.id.endsWith(by) ||
89
+ Trax.names[g.rules].toLowerCase().includes(by) ||
90
+ g.p1.toLowerCase().includes(by) ||
91
+ g.p2.toLowerCase().includes(by) ||
92
+ g.moves.startsWith(by + ' '),
93
+ )
94
+ }
95
+ }
96
+
97
+ if (reverse) list.reverse()
98
+
99
+ return list
100
+ }
101
+
102
+ listCmd.fn = (CLI, ...filters) => {
103
+ if (Object.keys(CLI.GAMES).length === 0) {
104
+ return CLI.error('No games. Type "new" to start a new game.')
105
+ }
106
+
107
+ let list = []
108
+ const size = {
109
+ id: 1,
110
+ name: 1,
111
+ moves: 1,
112
+ p1: 1,
113
+ p2: 1,
114
+ }
115
+
116
+ for (const game of Object.values(CLI.GAMES)) {
117
+ const moves = game.moves.length === 0 ? 0 : game.moves.split(' ').length
118
+ list.push({
119
+ sid: String(game.id).padStart(9, '0'),
120
+ id: (game.id === CLI.GAME.id ? '* #' : '#') + String(game.id),
121
+ name: game.name?.length || 0,
122
+ rules: game.rules,
123
+ moves: plural(moves, 'move'),
124
+ smoves: game.over ? '9999' : String(moves).padStart(9, '0'),
125
+ sturn: String(game.turn),
126
+ p1: game.players?.[0] || 'white',
127
+ p2: game.players?.[1] || 'black',
128
+ turn: game.turn,
129
+ over: game.over,
130
+ })
131
+ }
132
+
133
+ list = listSort(list, filters)
134
+
135
+ for (const game of list) {
136
+ size.id = Math.max(size.id, game.id.length)
137
+ size.name = Math.max(size.name, game.name)
138
+ size.moves = Math.max(size.moves, game.moves.length)
139
+ size.p1 = Math.max(size.p1, game.p1.length)
140
+ size.p2 = Math.max(size.p2, game.p2.length)
141
+ }
142
+
143
+ for (const game of list) {
144
+ CLI.out(
145
+ CLI.color(game.id.padStart(size.id) + ' ') +
146
+ Trax.names[game.rules] +
147
+ ' '.repeat(size.name - game.name + 1) +
148
+ CLI.bubble(game.turn === 1 ? 'wh' : 'w', game.p1) +
149
+ CLI.color('vs '.padStart(size.p1 - game.p1.length + 4)) +
150
+ CLI.bubble(game.turn === 2 ? 'bh' : 'b', game.p2) +
151
+ CLI.color(' '.padStart(size.p2 - game.p2.length + 1)) +
152
+ CLI.color(game.over ? 'game over' : game.moves),
153
+ )
154
+ }
155
+ }
@@ -0,0 +1,131 @@
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', 'Sam'],
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
+ ['Micky', '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
+ return pairs[Math.floor(Math.random() * pairs.length)]
93
+ }
94
+
95
+ newCmd.fn = (CLI, variant, ...names) => {
96
+ let rules = ''
97
+ if (variant) {
98
+ variant = variant.toLowerCase()
99
+
100
+ if (['8x8', '8', 'trax8x8', '8trax', '8x8trax'].includes(variant)) {
101
+ rules = 'trax8'
102
+ }
103
+
104
+ if (['loop', 'l', 'looptrax'].includes(variant)) rules = 'traxloop'
105
+ if (variant === 't') rules = 'trax'
106
+ if (['trax', 'traxloop', 'trax8'].includes(variant)) rules = variant
107
+
108
+ if (!rules) {
109
+ if (names.includes('vs') || names.includes('VS')) {
110
+ // We don't have a variant, we have player names
111
+ names.unshift(variant)
112
+ } else {
113
+ CLI.error('Unknown Trax variant.')
114
+ return CLI.do('help', 'new')
115
+ }
116
+ }
117
+ }
118
+
119
+ const players = pairUp()
120
+ if (names.length > 0) {
121
+ let vs = names.indexOf('vs')
122
+ if (vs === -1) vs = names.indexOf('VS')
123
+ if (vs >= 0) {
124
+ players[0] = names.slice(0, vs).join(' ') || 'white'
125
+ players[1] = names.slice(vs + 1).join(' ') || 'black'
126
+ }
127
+ }
128
+
129
+ const id = CLI.newGame(rules, players, '')
130
+ CLI.out(CLI.color('Started new game #' + id))
131
+ }