@slugbugblue/trax 0.12.0 → 0.13.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,20 @@
1
1
  # @slugbugblue/trax
2
2
 
3
+ ## 0.13.0 - 2022-11-25
4
+
5
+ - Add move suggestions to `analyst.js` for CLI and bot use
6
+
7
+ - use `suggest(trax)` to get a suggestion for the current position
8
+ - added the `suggest` CLI command for quick access to a random suggestion
9
+
10
+ - Use font glyphs for text bubbles in `tty.js` only if one of the following
11
+ environment variables are defined and non-empty:
12
+
13
+ - `NERDFONT`, `POWERLINE`, `P9K_TTY`, `P9K_SSH`
14
+
15
+ - Begin work on documenting the javascript files using jsdoc, with the initial
16
+ focus on types so typescript-powered autocomplete can be more useful
17
+
3
18
  ## 0.12.0 - 2022-10-28
4
19
 
5
20
  - `point.in()` now accepts any two opposing corners to define the bounding box
package/README.md CHANGED
@@ -5,8 +5,8 @@ Trax game-playing engine and etc.
5
5
  ## Description
6
6
 
7
7
  Trax is a two-player boardless board game invented by David Smith. For more
8
- information about Trax, including its rules and history, see the official
9
- website at [traxgame.com][traxgame].
8
+ information about Trax, including its [rules][traxrules] and
9
+ [history][traxhistory], see the official website at [traxgame.com][traxgame].
10
10
 
11
11
  This goal of this javascript project is to allow Trax to be played
12
12
  programmatically. It can be used as an engine in a web browser, for example at
@@ -95,7 +95,9 @@ Examples:
95
95
 
96
96
  ## API Usage
97
97
 
98
- The main Trax engine is provided as `engine.js`, which can be used as follows:
98
+ ### Trax engine
99
+
100
+ The main Trax engine is provided in `engine.js`, which can be used as follows:
99
101
 
100
102
  ```javascript
101
103
  import { Trax } from '@slugbugblue/trax'
@@ -115,12 +117,45 @@ let puzzle = new Trax('trax', '@0/ a0\\ @2/')
115
117
  puzzle.play('@2\\')
116
118
  ```
117
119
 
118
- For specifics, see the [engine.js documentation][engine-docs].
120
+ For specifics, see the [engine.js documentation][docs-engine].
121
+
122
+ ### Analysis
123
+
124
+ The analysis engine is provided in `analyst.js`, and it can be used as follows:
125
+
126
+ ```javascript
127
+ import { Trax } from '@slugbugblue/trax'
128
+ import { analyze } from '@slugbugblue/trax/analyst'
129
+
130
+ let trax = new Trax('trax', '@0/ @1\\')
131
+
132
+ let analysis = analyze(trax)
133
+
134
+ console.log('Edge analysis:', analysis.edge)
135
+ console.log('Threat analysis:', analysis.threats)
136
+ console.log('Score analysis:', analysis.scores)
137
+ ```
138
+
139
+ For additional information, refer to the [analyst.js
140
+ documentation][docs-analyst].
141
+
142
+ ### Threats database
143
+
144
+ The threats database is currently hand-coded in `threats.js`, so refer to that
145
+ file if you want to try to update the analysis engine's understanding of
146
+ threats.
147
+
148
+ ### Puzzles database
149
+
150
+ The puzzles database is provided in `puzzles.js`, and so far just contains
151
+ puzzles that I've been using to test the analysis engine, along with the list of
152
+ publicly available puzzles at [the traxgame.com puzzles page][traxpuzzles].
119
153
 
120
154
  ## Roadmap
121
155
 
122
- - Game position analysis
123
- - Puzzles and Puzzlebot
156
+ - Improve the threats database / analysis engine.
157
+ - Add a puzzle interface to the CLI.
158
+ - Add more puzzles.
124
159
 
125
160
  ## Support
126
161
 
@@ -174,12 +209,16 @@ David Smith and heirs, and are not to be used without permission.
174
209
 
175
210
  [ctrans]: mailto:chad@transtrum.net
176
211
  [dgb]: mailto:donald@traxgame.com
177
- [engine-docs]: docs/engine.md
212
+ [docs-engine]: docs/engine.md
213
+ [docs-analyst]: docs/analyst.md
178
214
  [env-path]: https://www.npmjs.com/package/env-paths
179
215
  [goldtoken]: https://goldtoken.com/
180
216
  [repo]: https://gitlab.com/slugbugblue/trax
181
217
  [sbb]: https://slugbugblue.com/
182
218
  [traxbook]: http://traxgame.com/shop_book.php
183
219
  [traxgame]: http://traxgame.com/
220
+ [traxhistory]: http://www.traxgame.com/about_history.php
221
+ [traxpuzzles]: http://www.traxgame.com/games_puzzles.php
222
+ [traxrules]: http://www.traxgame.com/about_rules.php
184
223
  [xdg]:
185
224
  https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
package/docs/analyst.md CHANGED
@@ -7,7 +7,7 @@ CLI and for bots.
7
7
 
8
8
  ```javascript
9
9
  import { Trax } from '@slugbugblue/trax'
10
- import { analyze } from '@slugbugblue/trax/analyst'
10
+ import { analyze, suggest } from '@slugbugblue/trax/analyst'
11
11
 
12
12
  const trax = new Trax('trax', '@0/ @1/ B0\\')
13
13
 
@@ -16,6 +16,12 @@ const analysis = analyze(trax)
16
16
  console.log(JSON.stringify(analysis.edge, null, 2))
17
17
  console.log(JSON.stringify(analysis.threats, null, 2))
18
18
  console.log(JSON.stringify(analysis.scores, null, 2))
19
+
20
+ const suggestion = suggest(trax)
21
+
22
+ console.log(JSON.stringify(suggestion.options, null, 2))
23
+
24
+ trax.play(suggestion.pick.move)
19
25
  ```
20
26
 
21
27
  ## API
@@ -107,7 +113,7 @@ simple count of the threats at each level can give a good first guess at the
107
113
  "score" of the current position. Each element of the array is an object with the
108
114
  following keys:
109
115
 
110
- - `pattern`: The threat pattern as it was fed into the threats database. This is
116
+ - `threat`: The threat pattern as it was fed into the threats database. This is
111
117
  a simple edge string.
112
118
  - `match`: The portion of the actual edge string that matched the pattern, which
113
119
  may or may not be exactly the same as the pattern.
@@ -144,6 +150,42 @@ For any given location, determine the type of playable space. Returns one of:
144
150
  - `C`: This location is in a cave, and certain plays at this position result in
145
151
  illegal moves.
146
152
 
153
+ ### Suggestion class
154
+
155
+ Accessible from `suggest(game)`, this class is returned with several useful
156
+ pieces of information.
157
+
158
+ #### properties
159
+
160
+ ##### `all`
161
+
162
+ A list of all the moves analyzed, ordered by highest scoring move first.
163
+
164
+ Each move is an object with the following properties:
165
+
166
+ - `move`: the notation of the move
167
+ - `score`: the score of the move
168
+ - `analysis`: the analysis object of the position for this move
169
+
170
+ ##### `options`
171
+
172
+ A smaller subset of the `all` list, with only the moves that are worth
173
+ considering.
174
+
175
+ ##### `pick`
176
+
177
+ A random selection of one of the `options`.
178
+
179
+ ##### `ms`
180
+
181
+ The number of milliseconds spent performing the analyses required to create the
182
+ suggestion.
183
+
184
+ #### computed properties
185
+
186
+ - `best`: quick access to the top-scoring move
187
+ - `analyzed`: quick access to a count of the number of moves analyzed
188
+
147
189
  ## License
148
190
 
149
191
  Copyright 2019-2022 Chad Transtrum
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slugbugblue/trax",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Trax game engine and friends",
5
5
  "keywords": [
6
6
  "trax",
@@ -11,16 +11,15 @@
11
11
  "bugs": "https://gitlab.com/slugbugblue/trax/issues",
12
12
  "license": "Apache-2.0",
13
13
  "author": "Chad Transtrum <chad@transtrum.net>",
14
+ "types": "./src/types.d.ts",
14
15
  "exports": {
15
16
  ".": "./src/engine.js",
16
- "./analyst": "./src/analyst.js",
17
- "./analyst.js": "./src/analyst.js",
18
17
  "./point": "./src/point.js",
19
- "./point.js": "./src/point.js",
18
+ "./analyst": "./src/analyst.js",
19
+ "./puzzles": "./src/puzzles.js",
20
20
  "./threats": "./src/threats.js",
21
- "./threats.js": "./src/threats.js",
22
21
  "./tty": "./src/tty.js",
23
- "./tty.js": "./src/tty.js"
22
+ "./version": "./src/version.js"
24
23
  },
25
24
  "repository": "gitlab:slugbugblue/trax",
26
25
  "bin": {
@@ -38,7 +37,7 @@
38
37
  "yaml": "^2.0.0-11"
39
38
  },
40
39
  "devDependencies": {
41
- "ava": "^4.1.0",
40
+ "ava": "^5.1.0",
42
41
  "c8": "^7.11.0",
43
42
  "genversion": "^3.0.2",
44
43
  "prettier": "^2.6.1",
package/src/analyst.js CHANGED
@@ -15,10 +15,10 @@
15
15
 
16
16
  // Analyst module: provides analysis functions for Trax games
17
17
 
18
- import { Point } from '@slugbugblue/trax/point.js'
18
+ import { Point } from '@slugbugblue/trax/point'
19
19
  import { Trax } from '@slugbugblue/trax'
20
20
 
21
- import { threats } from '@slugbugblue/trax/threats.js'
21
+ import { threats } from '@slugbugblue/trax/threats'
22
22
 
23
23
  // Helpers
24
24
  const id = () => {
@@ -31,7 +31,17 @@ const id = () => {
31
31
  return 'analysis-' + code
32
32
  }
33
33
 
34
- // Make parseInt Array.map()-friendly
34
+ // A sort function for hashes with a numeric score property
35
+ const byScoreDesc = (a, b) => b.score - a.score
36
+
37
+ // A filter function factory to remove scores not within x points of the highest score
38
+ const withinX = (highest, tolerance) => (s) =>
39
+ s.score === highest || highest - s.score <= tolerance
40
+
41
+ // Return a random element of an array
42
+ const randomDraw = (array) => array[Math.floor(Math.random() * array.length)]
43
+
44
+ // Make parseInt a little shorter
35
45
  const int = (s) => Number.parseInt(s, 10)
36
46
 
37
47
  const leftTurn = { d: 'r', l: 'd', r: 'u', u: 'l' }
@@ -160,18 +170,41 @@ const label = (rawEdge) => {
160
170
  return final
161
171
  }
162
172
 
173
+ const threatValues = (threatsArray) => {
174
+ return threatsArray.length
175
+ // Hm. Maybe we don't need this ...
176
+ // let count = 0
177
+ // for (const threat of threatsArray) count += threat.value
178
+ // return count
179
+ }
180
+
163
181
  const attackScore = (from, level) => {
164
- const count = from[level]?.length ?? 0
182
+ const count = threatValues(from[level] || [])
165
183
  if (level < 1) return count
166
184
  return count * 10 ** (threats.maxDepth - level + 1)
167
185
  }
168
186
 
169
- // Shortcut to get the number of active threats
170
- const threatCount = (from) =>
171
- Object.entries(from).filter(([d, l]) => d > 0 && l.length > 0).length
187
+ // Shortcut to get the number of attacks
188
+ const attackCount = (from) => threatValues(from['1'] || [])
189
+
190
+ // Shortcut to get the number of passive threats
191
+ const threatCount = (from) => {
192
+ let count = 0
193
+ for (const [depth, array] of Object.entries(from)) {
194
+ count += int(depth) > 1 ? threatValues(array) : 0
195
+ }
196
+
197
+ return count
198
+ }
199
+
200
+ const passiveAttack = (from) =>
201
+ attackCount(from) > 1 || (attackCount(from) === 1 && threatCount(from) > 0)
202
+
203
+ const underAttack = (from) => attackCount(from) + threatCount(from) > 0
172
204
 
173
205
  // Analysis class
174
206
  export const Analysis = class {
207
+ /** @type TraxVariant */
175
208
  #rules = 'trax'
176
209
  #id = 'analysis'
177
210
  #moves = []
@@ -255,17 +288,14 @@ export const Analysis = class {
255
288
  const inactive = this.threats[Trax.other(game.color)]
256
289
  let advantage = Number.POSITIVE_INFINITY
257
290
 
258
- // First examine the player who has the initiative
291
+ // First examine the active player
259
292
  for (const level of Object.keys(active).map((i) => int(i))) {
260
293
  if (level > 0 && level < advantage) advantage = level
261
294
  score += attackScore(active, level)
262
295
  }
263
296
 
264
297
  const passive = game.color !== color
265
- const calc =
266
- !passive ||
267
- threatCount(this.threats[color]) > 1 ||
268
- threatCount(this.threats[Trax.other(color)]) > 0
298
+ const calc = !passive || passiveAttack(inactive) || underAttack(active)
269
299
 
270
300
  // Calculate the effects of the passive player first
271
301
  for (const level of Object.keys(inactive).map((i) => int(i))) {
@@ -406,6 +436,7 @@ export const Analysis = class {
406
436
  threat: threat.pattern,
407
437
  match: match[0],
408
438
  at: match.index,
439
+ value: threat.value,
409
440
  })
410
441
  }
411
442
  }
@@ -449,3 +480,56 @@ export const Analysis = class {
449
480
  }
450
481
 
451
482
  export const analyze = (game) => new Analysis(game)
483
+
484
+ export const Suggestion = class {
485
+ all = []
486
+ options = []
487
+ pick = {}
488
+ ms = 0
489
+
490
+ constructor(analyzed, ms) {
491
+ this.all = analyzed.sort(byScoreDesc)
492
+ const best = analyzed[0] || { move: '', score: 0 }
493
+ this.options = analyzed.filter(withinX(best.score, 5))
494
+ this.pick = randomDraw(this.options) || best
495
+ this.ms = ms
496
+ }
497
+
498
+ get best() {
499
+ return this.all[0]
500
+ }
501
+
502
+ get analyzed() {
503
+ return this.all.length
504
+ }
505
+
506
+ // Make a nice representation for the nodejs REPL
507
+ [Symbol.for('nodejs.util.inspect.custom')](_, options) {
508
+ const style = options.stylize
509
+ let out = style(this.pick.move, 'string')
510
+ out += ' ' + style(this.pick.score, 'number') + ' ['
511
+ out += this.options
512
+ .slice(0, 5)
513
+ .map((s) => style(s.move, 'string') + ' ' + style(s.score, 'number'))
514
+ .join(', ')
515
+ out += '] ' + this.analyzed + ' in ' + this.ms + 'ms'
516
+ return out
517
+ }
518
+ }
519
+
520
+ export const suggest = (game) => {
521
+ const start = Date.now()
522
+ const analyzed = {}
523
+ const { color, moves, rules } = game
524
+ for (const move of game.possibleMoves()) {
525
+ const trax = new Trax(rules, moves)
526
+ trax.play(move)
527
+ const unique = trax.normalized
528
+ if (!analyzed[unique]) {
529
+ const analysis = analyze(trax)
530
+ analyzed[unique] = { move, score: analysis.scores[color], analysis }
531
+ }
532
+ }
533
+
534
+ return new Suggestion(Object.values(analyzed), Date.now() - start)
535
+ }
package/src/cli.js CHANGED
@@ -21,9 +21,9 @@ import repl from 'node:repl'
21
21
 
22
22
  import YAML from 'yaml'
23
23
 
24
- import { Trax } from './engine.js'
25
- import { version } from './version.js'
26
- import * as tty from './tty.js'
24
+ import { Trax } from '@slugbugblue/trax'
25
+ import { version } from '@slugbugblue/trax/version'
26
+ import * as tty from '@slugbugblue/trax/tty'
27
27
 
28
28
  // Load all the commands as individual plugins
29
29
  import { analyzeCmd } from './cmds/analyze.js'
@@ -40,6 +40,7 @@ import { listCmd } from './cmds/list.js'
40
40
  import { newCmd } from './cmds/new.js'
41
41
  import { playCmd, tryCmd } from './cmds/play-try.js'
42
42
  import { selectCmd } from './cmds/select.js'
43
+ import { suggestCmd } from './cmds/suggest.js'
43
44
  import { undoCmd } from './cmds/undo.js'
44
45
  import { viewCmd } from './cmds/view.js'
45
46
 
@@ -88,6 +89,7 @@ const plugins = [
88
89
  playCmd,
89
90
  quitCmd,
90
91
  selectCmd,
92
+ suggestCmd,
91
93
  tryCmd,
92
94
  undoCmd,
93
95
  versionCmd,
@@ -111,7 +113,10 @@ const newId = (DATA) => {
111
113
  return String(min - 1)
112
114
  }
113
115
 
114
- // Colorize text according to simplistic and not very complete patterns
116
+ /** Colorize text according to simplistic and not very complete patterns.
117
+ * @type Colorer
118
+ */
119
+ // @ts-ignore
115
120
  const color = (text, def) => {
116
121
  let out = ''
117
122
  let quote = false
@@ -121,7 +126,7 @@ const color = (text, def) => {
121
126
  const short = (t) => tty.color(t, CLI.COLORS.short) + c()
122
127
  for (const char of text) {
123
128
  switch (char) {
124
- case '"':
129
+ case '"': {
125
130
  if (quote) {
126
131
  colors.pop()
127
132
  } else {
@@ -131,31 +136,45 @@ const color = (text, def) => {
131
136
  out += c('')
132
137
  quote = !quote
133
138
  break
134
- case '#':
139
+ }
140
+
141
+ case '#': {
135
142
  id = true
136
143
  colors.push(CLI.COLORS.id)
137
144
  out += c('#')
138
145
  break
139
- case '|':
146
+ }
147
+
148
+ case '|': {
140
149
  out += short('|')
141
150
  break
142
- case '[':
151
+ }
152
+
153
+ case '[': {
143
154
  colors.push(CLI.COLORS.optional)
144
155
  out += short('[')
145
156
  break
146
- case ']':
157
+ }
158
+
159
+ case ']': {
147
160
  colors.pop()
148
161
  out += short(']')
149
162
  break
150
- case '<':
163
+ }
164
+
165
+ case '<': {
151
166
  colors.push(CLI.COLORS.variable)
152
167
  out += c('')
153
168
  break
154
- case '>':
169
+ }
170
+
171
+ case '>': {
155
172
  colors.pop()
156
173
  out += c('')
157
174
  break
158
- default:
175
+ }
176
+
177
+ default: {
159
178
  if (id && !/[\da-z]/.test(char)) {
160
179
  id = false
161
180
  colors.pop()
@@ -163,6 +182,7 @@ const color = (text, def) => {
163
182
  } else {
164
183
  out += char
165
184
  }
185
+ }
166
186
  }
167
187
  }
168
188
 
@@ -170,7 +190,7 @@ const color = (text, def) => {
170
190
  }
171
191
 
172
192
  // Internal representations of files
173
- const CONFIG = { id: 0 }
193
+ const CONFIG = { id: '' }
174
194
  const DATA = { games: {} }
175
195
 
176
196
  // CLI context object for plugins
@@ -550,11 +570,34 @@ const completer = (line) => {
550
570
  return [completions, match]
551
571
  }
552
572
 
573
+ const ambiguousSuggestions = (cmd) => {
574
+ const maybe = new Set()
575
+ for (const alias of cmd) {
576
+ if (alias.startsWith(CLI.CMD)) maybe.add(alias)
577
+
578
+ let alts = commands[alias].alt
579
+ alts = is(alts, 'arr') ? alts : [alts || '']
580
+
581
+ for (const alt of alts) {
582
+ if (alt.startsWith(CLI.CMD)) maybe.add(alt)
583
+ }
584
+ }
585
+
586
+ return maybe
587
+ }
588
+
553
589
  const evaluate = async (command) => {
554
590
  const [CMD, ...words] = command.split(/\s+/).filter(Boolean)
555
- const cmdLower = CMD.toLowerCase()
591
+ const cmdLower = CMD?.toLowerCase() || ''
556
592
  CLI.CMD = cmdLower
557
- const cmd = abbreviations[cmdLower]
593
+ let cmd = abbreviations[cmdLower]
594
+
595
+ // If we are not in a REPL, let's remove hidden commands
596
+ if (!CLI.repl && is(cmd, 'arr')) {
597
+ cmd = cmd.filter((c) => !commands[c].hide)
598
+ if (cmd.length === 1) cmd = cmd[0]
599
+ }
600
+
558
601
  if (is(cmd, 'str')) {
559
602
  if (words.length > 0 && abbreviations[words[0].toLowerCase()] === 'help') {
560
603
  commands.help.fn(CLI, cmd)
@@ -563,18 +606,7 @@ const evaluate = async (command) => {
563
606
  }
564
607
  } else if (is(cmd, 'arr')) {
565
608
  CLI.error('Ambiguous command. Perhaps you meant one of the following:')
566
- const maybe = new Set()
567
-
568
- for (const alias of cmd) {
569
- if (alias.startsWith(cmdLower)) maybe.add(alias)
570
-
571
- let alts = commands[alias].alt
572
- alts = is(alts, 'arr') ? alts : [alts || '']
573
-
574
- for (const alt of alts) {
575
- if (alt.startsWith(cmdLower)) maybe.add(alt)
576
- }
577
- }
609
+ const maybe = ambiguousSuggestions(cmd)
578
610
 
579
611
  if (maybe.size > 0) {
580
612
  for (const alias of [...maybe].sort()) {
@@ -593,8 +625,6 @@ const evaluate = async (command) => {
593
625
  CLI.error('Unknown command. Type "help" for a list of valid commands.')
594
626
  }
595
627
  }
596
-
597
- return undefined
598
628
  }
599
629
 
600
630
  const main = async () => {
@@ -630,4 +660,4 @@ const main = async () => {
630
660
  }
631
661
  }
632
662
 
633
- main()
663
+ await main()
@@ -15,8 +15,8 @@
15
15
 
16
16
  // CLI analyze command
17
17
 
18
- import { analyze } from '../analyst.js'
19
- import { Trax } from '../engine.js'
18
+ import { analyze } from '@slugbugblue/trax/analyst'
19
+ import { Trax } from '@slugbugblue/trax'
20
20
 
21
21
  const notationRx = /^[@a-z]+\d+[bps/\\+]$/i
22
22
 
@@ -21,7 +21,7 @@ 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
25
 
26
26
  const paths = envPaths('trax', { suffix: '' })
27
27
  const PATHS = {}
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',
@@ -41,7 +41,7 @@ export const listCmd = {
41
41
  }
42
42
 
43
43
  const plural = (n, noun, nouns) =>
44
- n + ' ' + (Math.abs(n) === 1 ? noun : nouns ? nouns : noun + 's')
44
+ n + ' ' + (Math.abs(n) === 1 ? noun : nouns || noun + 's')
45
45
 
46
46
  const alphaSort = (a, b) => {
47
47
  a = String(a).toLowerCase()
package/src/cmds/new.js CHANGED
@@ -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) => {
@@ -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' ? 'wh' : 'bh',
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
+ }