@slugbugblue/trax 0.12.0 → 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.
@@ -21,7 +21,8 @@ import process from 'node:process'
21
21
  import envPaths from 'env-paths'
22
22
  import makeDir from 'make-dir'
23
23
 
24
- import { Trax } from '../engine.js'
24
+ import { Trax } from '@slugbugblue/trax'
25
+ import { puzzles, sources } from '@slugbugblue/trax/puzzles'
25
26
 
26
27
  const paths = envPaths('trax', { suffix: '' })
27
28
  const PATHS = {}
@@ -30,7 +31,12 @@ const PATHS = {}
30
31
  let cli = {}
31
32
 
32
33
  const fsOuch = (fsError) => {
33
- cli.out(fsError, cli.COLORS.fatal)
34
+ if (cli.out) {
35
+ cli.out(fsError, cli.COLORS.fatal)
36
+ } else {
37
+ console.log(fsError)
38
+ }
39
+
34
40
  throw fsError
35
41
  }
36
42
 
@@ -72,7 +78,12 @@ export const getFile = async (filetype, filename, variable, decode) => {
72
78
  }
73
79
  } catch (fsError) {
74
80
  if (fsError instanceof TypeError || fsError instanceof SyntaxError) {
75
- cli.error(`Unable to read ${filetype} file ${path}/${filename}`)
81
+ const error = `Unable to read ${filetype} file ${path}/${filename}`
82
+ if (cli.error) {
83
+ cli.error(error)
84
+ } else {
85
+ console.log(error)
86
+ }
76
87
  } else if (fsError.code !== 'ENOENT') {
77
88
  fsOuch(fsError)
78
89
  }
@@ -200,6 +211,25 @@ exportCmd.fn = (CLI, id, filename) => {
200
211
  content += ' vs '
201
212
  content += game.players?.[1] || 'black'
202
213
  content += '\n; @slugbugblue/trax cli.js v' + CLI.version + '\n'
214
+ if (game.puzzle) {
215
+ const puzzle = puzzles.find((p) => p.id === game.puzzle)
216
+ if (puzzle) {
217
+ const source = sources[puzzle.src]
218
+ content += '; Puzzle ' + game.puzzle
219
+ if (source) {
220
+ content += ' provided courtesy of ' + source.name
221
+ if (source.url) content += ' (' + source.url + ')'
222
+ }
223
+
224
+ if (puzzle.title) content += '\n; ' + puzzle.title
225
+ if (puzzle.desc) content += '\n; ' + puzzle.desc
226
+ content += '\n; Starting position ' + puzzle.notation + '\n; '
227
+ const players = [0, 'white', 'black']
228
+ content += players[puzzle.player] + ' to win by move '
229
+ content += String(puzzle.max) + '\n'
230
+ }
231
+ }
232
+
203
233
  content += trax.notation + '\n'
204
234
  if (filename) filename = filename.replace(/[^ a-z\d.~/]/g, '')
205
235
  if (filename) {
package/src/cmds/list.js CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  // CLI list command
17
17
 
18
- import { Trax } from '../engine.js'
18
+ import { Trax } from '@slugbugblue/trax'
19
19
 
20
20
  export const listCmd = {
21
21
  name: 'list',
@@ -40,9 +40,6 @@ export const listCmd = {
40
40
  ],
41
41
  }
42
42
 
43
- const plural = (n, noun, nouns) =>
44
- n + ' ' + (Math.abs(n) === 1 ? noun : nouns ? nouns : noun + 's')
45
-
46
43
  const alphaSort = (a, b) => {
47
44
  a = String(a).toLowerCase()
48
45
  b = String(b).toLowerCase()
@@ -120,7 +117,7 @@ listCmd.fn = (CLI, ...filters) => {
120
117
  id: (game.id === CLI.GAME.id ? '* #' : '#') + String(game.id),
121
118
  name: game.name?.length || 0,
122
119
  rules: game.rules,
123
- moves: plural(moves, 'move'),
120
+ moves: CLI.plural(moves, 'move'),
124
121
  smoves: game.over ? '9999' : String(moves).padStart(9, '0'),
125
122
  sturn: String(game.turn),
126
123
  p1: game.players?.[0] || 'white',
package/src/cmds/new.js CHANGED
@@ -42,7 +42,7 @@ const pairs = [
42
42
  ['Donald', 'Daisy'],
43
43
  ['Fred', 'Barney'],
44
44
  ['Fred', 'George'],
45
- ['Frodo', 'Sam'],
45
+ ['Frodo', 'Samwise'],
46
46
  ['Han', 'Chewie'],
47
47
  ['Harry', 'Ron'],
48
48
  ['Holmes', 'Watson'],
@@ -58,13 +58,13 @@ const pairs = [
58
58
  ['Mario', 'Luigi'],
59
59
  ['Marlin', 'Dory'],
60
60
  ['Mary-Kate', 'Ashley'],
61
- ['Micky', 'Minnie'],
61
+ ['Mickey', 'Minnie'],
62
62
  ['Miss Piggy', 'Kermit'],
63
63
  ['Mork', 'Mindy'],
64
64
  ['Obi-Wan', 'Anakin'],
65
65
  ['Pan', 'Hook'],
66
66
  ['Phineas', 'Ferb'],
67
- ['Player 1', 'Player 2'],
67
+ ['player 1', 'player 2'],
68
68
  ['p1', 'p2'],
69
69
  ['R2-D2', 'C-3PO'],
70
70
  ['Ren', 'Stimpy'],
@@ -89,7 +89,9 @@ const pairs = [
89
89
  ]
90
90
 
91
91
  const pairUp = () => {
92
- return pairs[Math.floor(Math.random() * pairs.length)]
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
93
95
  }
94
96
 
95
97
  newCmd.fn = (CLI, variant, ...names) => {
@@ -15,6 +15,8 @@
15
15
 
16
16
  // CLI play/try commands
17
17
 
18
+ import { suggest } from '@slugbugblue/trax/analyst'
19
+
18
20
  const moveRx = /^\d+\.?$/
19
21
  const notationRx = /^[@a-z]+\d+[bps/\\+]$/i
20
22
 
@@ -46,6 +48,40 @@ export const tryCmd = {
46
48
  ],
47
49
  }
48
50
 
51
+ const bot = (CLI) => {
52
+ const game = CLI.GAME
53
+ const trax = CLI.TRAX
54
+ const players = game.players || ['a', 'b']
55
+ const name = players[trax.turn - 1]
56
+ if (trax.over && game.puzzle) {
57
+ if (trax.move <= game.max && name !== 'puzzlebot') {
58
+ CLI.puzzleSolved(game.puzzle)
59
+ return 'win'
60
+ }
61
+
62
+ return 'lose'
63
+ }
64
+
65
+ if (!trax.over && /bot\b/i.test(name)) {
66
+ if (game.puzzle && trax.move >= game.max) {
67
+ trax.play('puzzled')
68
+ return 'lose'
69
+ }
70
+
71
+ CLI.do('view')
72
+ CLI.out(CLI.bubble(trax.color + 'd', `${name} thinking...`))
73
+ const move = suggest(trax)?.pick?.move
74
+ if (move) {
75
+ CLI.out(CLI.bubble(trax.color + 'd', `${name} chooses ${move}`))
76
+ trax.play(move)
77
+ }
78
+
79
+ if (game.puzzle) return ' and win by move ' + String(game.max)
80
+ }
81
+
82
+ return ''
83
+ }
84
+
49
85
  playCmd.fn = (CLI, ...moves) => {
50
86
  if (moves.length === 0 || !moves[0]) {
51
87
  CLI.error('You must provide a move.')
@@ -56,24 +92,27 @@ playCmd.fn = (CLI, ...moves) => {
56
92
  CLI.do('new')
57
93
  }
58
94
 
59
- if (CLI.TRAX.over) {
95
+ const game = CLI.GAME
96
+ const trax = CLI.TRAX
97
+
98
+ if (trax.over) {
60
99
  return CLI.error('The game is over.')
61
100
  }
62
101
 
63
- const start = CLI.TRAX.move
102
+ const start = trax.move
64
103
  let checkMove
65
104
 
66
105
  for (let move of moves) {
67
106
  if (notationRx.test(move)) {
68
- const moveNumber = CLI.TRAX.move
107
+ const moveNumber = trax.move
69
108
  move = CLI.fixNotation(move)
70
109
  if (checkMove) {
71
- CLI.TRAX.play(checkMove, move)
110
+ trax.play(checkMove, move)
72
111
  } else {
73
- CLI.TRAX.play(move)
112
+ trax.play(move)
74
113
  }
75
114
 
76
- if (CLI.TRAX.move === moveNumber) {
115
+ if (trax.move === moveNumber) {
77
116
  CLI.error(
78
117
  'Move "' +
79
118
  (checkMove ? checkMove + '. ' : '') +
@@ -88,9 +127,17 @@ playCmd.fn = (CLI, ...moves) => {
88
127
  }
89
128
  }
90
129
 
91
- if (CLI.TRAX.move !== start) {
130
+ if (trax.move !== start) {
131
+ const result = bot(CLI)
92
132
  CLI.updateGameData()
93
133
  CLI.do('view')
134
+ if (result === 'lose') {
135
+ CLI.error('Failed to complete puzzle ' + game.puzzle)
136
+ } else if (result === 'win') {
137
+ CLI.out(CLI.color.success('You completed puzzle ' + game.puzzle))
138
+ } else if (result) {
139
+ CLI.out(result)
140
+ }
94
141
  }
95
142
  }
96
143
 
@@ -0,0 +1,152 @@
1
+ /** @file Puzzles CLI
2
+ * @copyright 2022
3
+ * @author Chad Transtrum <chad@transtrum.net>
4
+ * @license Apache-2.0
5
+ */
6
+
7
+ import { Trax } from '@slugbugblue/trax'
8
+ import { levels, puzzles, sources } from '@slugbugblue/trax/puzzles'
9
+
10
+ let maxLevel = 0
11
+ const puzzleIds = []
12
+ for (const puzzle of puzzles) {
13
+ maxLevel = Math.max(maxLevel, puzzle.level)
14
+ puzzleIds.push(puzzle.id)
15
+ }
16
+
17
+ const validLevels = levels.slice(0, maxLevel + 1)
18
+ const completions = [...validLevels, ...puzzleIds].join('|')
19
+
20
+ export const puzzlesCmd = {
21
+ name: 'puzzles',
22
+ args: '["list"|"start" [<difficulty>|<puzzleID>]]',
23
+ comp: 'list|ls|new|show|start|view ' + completions,
24
+ desc: 'play puzzles',
25
+ help: [
26
+ 'Practice your skills with puzzles. Use "list" to see the puzzle',
27
+ 'categories, or "start" to play a puzzle.',
28
+ ],
29
+ }
30
+
31
+ const showPuzzle = (CLI, id) => {
32
+ const puzzle = puzzles.find((p) => p.id === id)
33
+ if (!puzzle) return CLI.error(`Puzzle ${id} not found`)
34
+ const trax = new Trax(puzzle.game, puzzle.notation, 'cli')
35
+ const players = ['white', 'black']
36
+ CLI.display(trax, players)
37
+ let out = CLI.color.short(puzzle.id)
38
+ out += CLI.color(' is a ' + levels[puzzle.level] + ' puzzle') + '\n'
39
+ if (puzzle.title) out += CLI.color(puzzle.title) + '\n'
40
+ if (puzzle.desc) out += CLI.color.help(puzzle.desc) + '\n'
41
+ out += CLI.color(players[puzzle.player - 1] + ' to win by move ' + puzzle.max)
42
+ const src = sources[puzzle.src]
43
+ if (src) {
44
+ let source = '\nPuzzle '
45
+ source += src.copyright
46
+ ? 'copyright ' + src.copyright + ' by'
47
+ : 'courtesy of'
48
+ source += ' ' + src.name
49
+ const url = src.url ? ' ' + CLI.color.white(src.url) : ''
50
+ out += CLI.color.black(source) + url
51
+ }
52
+
53
+ CLI.out(out)
54
+ }
55
+
56
+ const listPuzzles = (CLI) => {
57
+ let size = 1
58
+ const data = []
59
+ for (const name of validLevels) {
60
+ size = Math.max(size, name.length)
61
+ data.push({ name, count: 0, solved: 0, selected: false })
62
+ }
63
+
64
+ if (data[CLI.puzzleLevel]) data[CLI.puzzleLevel].selected = true
65
+
66
+ for (const puzzle of puzzles) {
67
+ data[puzzle.level].count += 1
68
+ if (CLI.puzzle(puzzle.id).solved > 0) data[puzzle.level].solved += 1
69
+ }
70
+
71
+ for (const category of data) {
72
+ let out = category.selected ? CLI.color('* ') : ' '
73
+ out += CLI.color.short(category.name.padEnd(size + 1))
74
+ if (category.solved === 0) {
75
+ out += CLI.color(CLI.plural(category.count, 'puzzle'))
76
+ } else if (category.solved === category.count) {
77
+ if (category.count > 3) out += CLI.color.success('all ')
78
+ out += CLI.color.success(CLI.plural(category.count, 'puzzle') + ' solved')
79
+ } else {
80
+ out += CLI.color.success(`${category.solved} solved`)
81
+ out += CLI.color(` of ${category.count} puzzles`)
82
+ }
83
+
84
+ CLI.out(out)
85
+ }
86
+ }
87
+
88
+ const listLevel = (CLI, levelName) => {
89
+ let size = 1
90
+ const level = levels.indexOf(levelName)
91
+ const data = []
92
+ for (const puzzle of puzzles) {
93
+ if (puzzle.level !== level) continue
94
+ size = Math.max(size, puzzle.id.length)
95
+ data.push({
96
+ id: puzzle.id,
97
+ title: puzzle.title,
98
+ solved: CLI.puzzle(puzzle.id).solved > 0,
99
+ })
100
+ }
101
+
102
+ for (const puzzle of data) {
103
+ let out = CLI.color.success(puzzle.solved ? '✓' : ' ')
104
+ out += CLI.color.short(' ' + puzzle.id)
105
+ if (puzzle.title) out += CLI.color(' ' + puzzle.title)
106
+ CLI.out(out)
107
+ }
108
+ }
109
+
110
+ const startPuzzle = (CLI, puzzle) => {
111
+ const id = CLI.newPuzzle(puzzle)
112
+ CLI.out(CLI.color(`Started puzzle ${puzzle.id} as game #${id}`))
113
+ showPuzzle(CLI, puzzle.id)
114
+ }
115
+
116
+ const startNextPuzzle = (CLI, level) => {
117
+ if (level === undefined) level = CLI.puzzleLevel
118
+ const unsolved = puzzles.filter((p) => !CLI.puzzle(p.id).solved)
119
+ if (unsolved.length === 0) {
120
+ CLI.out(CLI.success('You have already completed all the puzzles.'))
121
+ return CLI.error('Specify a specific puzzle to try one again.')
122
+ }
123
+
124
+ const next = unsolved.find((p) => p.level >= level) || unsolved[0]
125
+ startPuzzle(CLI, next)
126
+ }
127
+
128
+ puzzlesCmd.fn = (CLI, action, id) => {
129
+ action = action?.toLowerCase() ?? 'list'
130
+ if ('start'.startsWith(action) || 'new'.startsWith(action)) {
131
+ if (!id) return startNextPuzzle(CLI)
132
+
133
+ const puzzle = puzzles.find((p) => p.id === id)
134
+ if (puzzle) {
135
+ return startPuzzle(CLI, puzzle)
136
+ }
137
+
138
+ if (validLevels.includes(id)) {
139
+ return startNextPuzzle(CLI, levels.indexOf(id))
140
+ }
141
+
142
+ return CLI.error('Could not start puzzle ' + id)
143
+ }
144
+
145
+ if (action) {
146
+ if (validLevels.includes(id)) return listLevel(CLI, id)
147
+ if (puzzleIds.includes(id)) return showPuzzle(CLI, id)
148
+ if (id) return CLI.error('Could not find puzzle ' + id)
149
+ }
150
+
151
+ listPuzzles(CLI)
152
+ }
@@ -0,0 +1,52 @@
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 suggest command
17
+
18
+ import { suggest } from '@slugbugblue/trax/analyst'
19
+
20
+ export const suggestCmd = {
21
+ name: 'suggest',
22
+ alt: ['bot'],
23
+ desc: 'suggest a move',
24
+ help: ['Suggest a move in the current game.'],
25
+ }
26
+
27
+ suggestCmd.fn = (CLI, debug) => {
28
+ if (!String(CLI.GAME?.id)) {
29
+ return CLI.error('No active game. Type "new" to start a game.')
30
+ }
31
+
32
+ const game = CLI.TRAX
33
+ const suggestion = suggest(game)
34
+
35
+ if (!suggestion.best) return
36
+
37
+ CLI.out(
38
+ CLI.bubble(
39
+ game.turn === 1 ? 'w' : 'b',
40
+ String(game.move + 1) + '. ' + suggestion.pick.move,
41
+ ),
42
+ )
43
+
44
+ if ('debug'.startsWith(debug)) {
45
+ const m = []
46
+ for (const move of suggestion.options) {
47
+ m.push(move.move + ' ' + move.score)
48
+ }
49
+
50
+ CLI.out('[' + m.join(', ') + '] ' + suggestion.ms + 'ms')
51
+ }
52
+ }
package/src/cmds/view.js CHANGED
@@ -49,5 +49,5 @@ viewCmd.fn = (CLI, move) => {
49
49
  CLI.color(' vs ') +
50
50
  CLI.bubble('b' + (trax.turn === 2 ? 'h' : ''), players[1]),
51
51
  )
52
- CLI.display(trax, players, false, show)
52
+ CLI.display(trax, players, undefined, show)
53
53
  }