@slugbugblue/trax 0.19.0 → 0.20.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/.dockerignore ADDED
@@ -0,0 +1,11 @@
1
+ .dockerignore
2
+ .git*
3
+ .husky
4
+ .npmignore
5
+ benchmark
6
+ coverage
7
+ Dockerfile
8
+ docs
9
+ jsconfig.json
10
+ node_modules
11
+ test
package/.gitlab-ci.yml ADDED
@@ -0,0 +1,90 @@
1
+ include:
2
+ - template: Security/SAST.gitlab-ci.yml
3
+ - template: Security/Secret-Detection.gitlab-ci.yml
4
+
5
+ stages:
6
+ - test
7
+ - sast
8
+ - benchmark
9
+ - release
10
+
11
+ sast:
12
+ stage: sast
13
+
14
+ nodejs-scan-sast:
15
+ allow_failure: false
16
+ rules:
17
+ - if: "$SAST_DISABLED"
18
+ when: never
19
+ - if: "$SAST_EXCLUDED_ANALYZERS =~ /nodejs-scan/"
20
+ when: never
21
+ - if: "$CI_COMMIT_BRANCH == 'main'"
22
+ exists:
23
+ - "**/package.json"
24
+
25
+ semgrep-sast:
26
+ allow_failure: false
27
+ rules:
28
+ - if: "$SAST_DISABLED"
29
+ when: never
30
+ - if: "$SAST_EXCLUDED_ANALYZERS =~ /semgrep/"
31
+ when: never
32
+ - if: "$CI_COMMIT_BRANCH == 'main'"
33
+ exists:
34
+ - "**/*.js"
35
+
36
+ secret_detection:
37
+ allow_failure: false
38
+
39
+ code-coverage:
40
+ stage: test
41
+ image: node:lts-alpine
42
+ cache:
43
+ key: lts
44
+ paths:
45
+ - node_modules/
46
+ script:
47
+ - node --version
48
+ - npm install
49
+ - npm test
50
+ coverage: /^All files\s*\|\s*([\d.]+)/
51
+
52
+ node16:
53
+ stage: test
54
+ image: node:16.15.0-alpine
55
+ cache:
56
+ key: node16
57
+ paths:
58
+ - node_modules/
59
+ script:
60
+ - node --version
61
+ - npm install
62
+ - npm test
63
+
64
+ benchmark:
65
+ stage: benchmark
66
+ image: node:lts-alpine
67
+ cache:
68
+ key: lts
69
+ paths:
70
+ - node_modules/
71
+ rules:
72
+ - if: $CI_COMMIT_TAG =~ /^v\d\+\.\d+\.\d+$/
73
+ script:
74
+ - npm run benchmark
75
+ - awk '/^## /{p++}p==1;p==2{exit}' CHANGELOG.md > RELEASE.$CI_COMMIT_TAG.md
76
+ artifacts:
77
+ paths:
78
+ - RELEASE.$CI_COMMIT_TAG.md
79
+ expire_in: 1 day
80
+
81
+ release:
82
+ stage: release
83
+ image: registry.gitlab.com/gitlab-org/release-cli:latest
84
+ rules:
85
+ - if: $CI_COMMIT_TAG =~ /^v\d\+\.\d+\.\d+$/
86
+ script:
87
+ - echo "Releasing $CI_COMMIT_TAG"
88
+ release:
89
+ tag_name: $CI_COMMIT_TAG
90
+ description: ./RELEASE.$CI_COMMIT_TAG.md
package/.husky/pre-commit CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env sh
2
2
  . "$(dirname -- "$0")/_/husky.sh"
3
3
 
4
+ npm outdated
4
5
  npm test
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @slugbugblue/trax
2
2
 
3
+ ## 0.20.0 - 2023-02-11
4
+
5
+ - Provide timing feedback on the puzzlebot's actions
6
+ - Add license information for puzzle sources
7
+ - Add gitlab CI configuration
8
+ - Drop support for node 14; node v16.15 is our minimum supported version
9
+ - More puzzles from Martin M. S. Pedersen -- bringing the total to 100+
10
+ - Enhance the docker configuration and document it in the README
11
+ - Refine faulty threat detection and add test for once case it was failing
12
+
3
13
  ## 0.19.0 - 2023-02-06
4
14
 
5
15
  - Added a simple LRU cache to increase the efficiency of the analysis engine
@@ -0,0 +1,113 @@
1
+ # Contributing to slugbugblue/trax
2
+
3
+ Hi, thanks for considering contributing to this project.
4
+
5
+ If this is your first time working on an open source project, it is recommended
6
+ that you start by reading [How to Contribute to Open Source][howto].
7
+
8
+ ## Ways to contribute
9
+
10
+ ### Improve documentation
11
+
12
+ One of the simplest ways to get started contributing is to find areas of the
13
+ documentation that are unclear, incorrect, or that have typos or other errors,
14
+ and providing feedback and corrections. You don't have to know how to code or
15
+ even how to use git.
16
+
17
+ ### Identify bugs
18
+
19
+ If you are aware of something not working as you think it should, feel free to
20
+ open a new issue to let us know. Browsing through the existing open issues and
21
+ adding comments or making clarifications can also be a great way to get started.
22
+
23
+ ### Fixing bugs
24
+
25
+ If you feel you have a solution to an existing bug, you are welcome to code up
26
+ your solution and submit a merge request. It may take some time to get fully up
27
+ to speed with our coding styles and with the structure of the code, so don't
28
+ feel bad if your merge request receives comments rather than being immediately
29
+ accepted. Simply make any necessary changes, and update the merge request, or
30
+ let us know in a subsequent comment if you think we are misunderstanding the
31
+ merge request.
32
+
33
+ ## Submitting an issue
34
+
35
+ If you choose to submit an issue, please take the time to ensure that you
36
+ provide as much detail as possible about the issue. A poorly worded or poorly
37
+ researched issue report will unnecessarily delay the issue resolution.
38
+
39
+ Even if you don't have the time or expertise to fully diagnose the problem, a
40
+ good issue report can be very helpful to us as we seek to improve the code.
41
+
42
+ ## Submitting a merge request
43
+
44
+ Good merge requests are a fantastic way to contribute to this project. Your
45
+ merge request is going to be accepted more quickly if it is easy to understand.
46
+ Ideally it should be focused on a single item and avoid unrelated commits.
47
+
48
+ Please ask first before embarking on a major change, such as adding a new
49
+ feature or refactoring the code, otherwise you risk spending a significant
50
+ amount of time working on something that will not be merged into the project.
51
+
52
+ If you have never submitted a merge request before, no fear. Here is a [quick
53
+ tutorial][howto-mr] on how to create one.
54
+
55
+ ### Merge request steps
56
+
57
+ - Fork the project and clone it to your own workspace.
58
+ - Get the latest changes and update the project dependencies:
59
+
60
+ ```bash
61
+ $ git checkout develop
62
+ $ git pull upstream develop
63
+ $ rm -rf node_modules
64
+ $ npm install
65
+ ```
66
+
67
+ - Create a new branch to contain your changes:
68
+
69
+ ```bash
70
+ $ git checkout -b <new-branch-name>
71
+ ```
72
+
73
+ - Make your changes.
74
+ - Test your changes.
75
+
76
+ ```bash
77
+ $ npm test
78
+ ```
79
+
80
+ - Push your changes back to your fork.
81
+
82
+ ```bash
83
+ $ git push -u origin <new-branch-name>
84
+ ```
85
+
86
+ 1. Open a merge request in the [slugbugblue/trax][trax-repo] repo.
87
+
88
+ ## Working with the code
89
+
90
+ This repository uses [XO][xo] for linting and [Prettier][prettier] for
91
+ formatting. Running `npm test` will produce an error if your changes do not
92
+ conform. You should consider installing the [XO plugin][xo-plugin] for your
93
+ editor to have the linting errors highlighted for you and automatically fixed on
94
+ save. You can also run `npx xo --fix` from the command line to fix most linting
95
+ errors.
96
+
97
+ This repository uses [AVA][ava] for writing and running tests. While we want as
98
+ much code coverage as possible for all code, we have a minimum requirement of
99
+ 95% of code coverage for the [analysis engine][analyst] and 100% for the [main
100
+ trax engine][engine]. You should be able to get a quick grasp on the testing
101
+ process by simply examining a few of the test files in the [test
102
+ directory][test].
103
+
104
+ [analyst]: src/analyst.js
105
+ [ava]: https://github.com/avajs/ava
106
+ [engine]: src/engine.js
107
+ [howto]: https://opensource.guide/how-to-contribute/
108
+ [howto-mr]: https://opensource.guide/how-to-contribute/#opening-a-pull-request
109
+ [prettier]: https://prettier.io
110
+ [test]: test/
111
+ [trax-repo]: https://gitlab.com/slugbugblue/trax
112
+ [xo]: https://github.com/sidresorhus/xo
113
+ [xo-plugin]: https://github.com/sidresorhus/xo#editor-plugins
package/Dockerfile CHANGED
@@ -1,9 +1,14 @@
1
1
  # Use the latest nodejs long-term-support release,
2
2
  # on the latest Alpine Linux for its small footprint
3
3
  FROM node:lts-alpine
4
- # Install current source code globally
5
- COPY . /src
6
- WORKDIR /src
7
- RUN npm install -g @slugbugblue/trax
4
+ # Install current source code
5
+ WORKDIR /trax
6
+ COPY . .
7
+ RUN npm install
8
+ # Ensure the data files can be persisted
9
+ WORKDIR /mnt/data
10
+ ENV XDG_CONFIG_HOME=/mnt/data
11
+ ENV XDG_DATA_HOME=/mnt/data
12
+ VOLUME /mnt/data
8
13
  # Use the trax CLI as the starting point
9
- ENTRYPOINT ["/usr/local/bin/trax"]
14
+ ENTRYPOINT ["/trax/src/cli.js"]
package/LICENSE CHANGED
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright 2021 Chad Transtrum
189
+ Copyright © 2021-2023 Chad Transtrum
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
package/README.md CHANGED
@@ -4,7 +4,7 @@ This project aims to provide tools for working with Trax games in Javascript.
4
4
 
5
5
  - Trax game-playing engine
6
6
  - Position analysis engine
7
- - Over 60 puzzles to help hone your Trax skills
7
+ - Over 100 puzzles to help hone your Trax skills
8
8
  - Command-line interface for ease of use
9
9
 
10
10
  ## Description
@@ -29,6 +29,9 @@ npm install @slugbugblue/trax
29
29
 
30
30
  # Or install globally to use the trax CLI more easily
31
31
  npm install -g @slugbugblue/trax
32
+
33
+ # Or create a docker image
34
+ docker build -t trax .
32
35
  ```
33
36
 
34
37
  ## CLI Usage
@@ -37,6 +40,19 @@ The command-line interface is provided as `trax`, available in your path if
37
40
  installed globally, or in the `node_modules/.bin` directory and runnable using
38
41
  `npx trax` if installed locally.
39
42
 
43
+ > If you installed by creating a Docker image, use the following command to
44
+ > launch the CLI:
45
+ >
46
+ > - `docker run --name trax -itv $HOME/trax:/mnt/data --rm trax`
47
+ >
48
+ > Since that's a bit much to type, it's recommended to create an alias in one of
49
+ > your startup scripts:
50
+ >
51
+ > - `alias trax="docker run --name trax -itv $HOME/trax:/mnt/data --rm trax"`
52
+ >
53
+ > Then you can launch the docker container simply by typing in `trax`, the same
54
+ > as if it were installed globally.
55
+
40
56
  The CLI can be used interactively by running the command with no parameters or
41
57
  by passing the `--interactive` or `-i` flag. When run interactively, you can
42
58
  avoid worries about the need to escape certain characters from the shell, and it
@@ -161,6 +177,8 @@ traxgame.com puzzles page][traxpuzzles], and [puzzles curated][puzzlebook] by
161
177
  ## Roadmap
162
178
 
163
179
  - Improve the threats database / analysis engine.
180
+ - Bot should be able to win all puzzles.
181
+ - Analysis should recognize line threats.
164
182
  - Add more puzzles.
165
183
 
166
184
  ## Support
@@ -170,8 +188,8 @@ Issues can be opened on the [gitlab project page][repo].
170
188
 
171
189
  ## Contributing
172
190
 
173
- Contributions are welcome. Ideally in the form of pull requests, but feel free
174
- to open an issue with a bug report or a suggestion as well.
191
+ [Contributions are welcome][contributing]. Ideally in the form of pull requests,
192
+ but feel free to open an issue with a bug report or a suggestion as well.
175
193
 
176
194
  ## Acknowledgments
177
195
 
@@ -188,10 +206,9 @@ always ready to share his insights on the mechanics of the game, as well as
188
206
  various approaches to encoding its complexities. His undeserved kindness and
189
207
  infinite patience in indulging my many questions can never be repaid.
190
208
 
191
- Thanks for [Martin M. S. Pedersen][traxplayer] for contributing ideas, code, and
192
- puzzles to this effort. He's been [curating Trax puzzles][puzzlebook] and
193
- [coding solutions for Trax][gnutrax] for over two decades, and brings a lot of
194
- expertise.
209
+ Thanks to [Martin M. S. Pedersen][traxplayer] for contributing ideas, code, and
210
+ puzzles. He's been [curating Trax puzzles][puzzlebook] and [coding solutions for
211
+ Trax][gnutrax] for over two decades, and brings a lot of expertise.
195
212
 
196
213
  ## License
197
214
 
@@ -218,6 +235,7 @@ The official rules for Trax are found at http://traxgame.com/about_rules.php.
218
235
  As Trax is a proprietary game, these rules are the intellectual property of
219
236
  David Smith and heirs, and are not to be used without permission.
220
237
 
238
+ [contributing]: CONTRIBUTING.md
221
239
  [ctrans]: mailto:chad@transtrum.net
222
240
  [dgb]: mailto:donald@traxgame.com
223
241
  [docs-engine]: docs/engine.md
package/docs/analyst.md CHANGED
@@ -119,11 +119,22 @@ following keys:
119
119
  - `match`: The portion of the actual edge string that matched the pattern, which
120
120
  may or may not be exactly the same as the pattern.
121
121
  - `at`: The index of the edge string at which the pattern match began.
122
+ - `value`: The multiplier for this threat. Most often this is `1`, but some
123
+ threats can be re-formed, and this will be `2` in those cases.
122
124
 
123
125
  ##### `faulty`
124
126
 
125
- Contains the same information as `threats` but for removed threats that were
126
- found to be faulty.
127
+ An object with two keys, `w` and `b`, each of which is a list of threats that
128
+ were found to be fauly.
129
+
130
+ Example data:
131
+
132
+ ```javascript
133
+ {
134
+ w: [{ threat: 'Arw?!pW', match: 'arwpa', at: 1, level: 2, value: 1 }],
135
+ b: [],
136
+ }
137
+ ```
127
138
 
128
139
  ##### `score`
129
140
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slugbugblue/trax",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Trax game engine and friends",
5
5
  "keywords": [
6
6
  "trax",
@@ -20,6 +20,7 @@
20
20
  "./puzzles": "./src/puzzles.js",
21
21
  "./threats": "./src/threats.js",
22
22
  "./tty": "./src/tty.js",
23
+ "./utils": "./src/utils.js",
23
24
  "./version": "./src/version.js"
24
25
  },
25
26
  "repository": "gitlab:slugbugblue/trax",
@@ -28,10 +29,12 @@
28
29
  },
29
30
  "scripts": {
30
31
  "benchmark": "node benchmark/puzzles.bench.js",
32
+ "genversion": "genversion --es6 src/version.js",
33
+ "git-add": "git add src/version.js benchmark/benchmarks.json",
31
34
  "test": "xo && c8 ava",
32
35
  "prepare": "husky install",
33
36
  "preversion": "npm test",
34
- "version": "genversion --es6 src/version.js && npm run benchmark || git add src/version.js benchmark/benchmarks.json",
37
+ "version": "npm run genversion ; npm run benchmark ; npm run git-add",
35
38
  "postversion": "git push && git push --tags && npm publish"
36
39
  },
37
40
  "dependencies": {
@@ -49,7 +52,14 @@
49
52
  },
50
53
  "type": "module",
51
54
  "engines": {
52
- "node": "^14.14.0 || >=16.0.0"
55
+ "node": ">=16.15.0"
56
+ },
57
+ "c8": {
58
+ "branches": 95,
59
+ "check-coverage": true,
60
+ "functions": 95,
61
+ "lines": 95,
62
+ "statements": 95
53
63
  },
54
64
  "prettier": {
55
65
  "bracketSpacing": true,
package/src/analyst.js CHANGED
@@ -204,7 +204,7 @@ const label = (rawEdge) => {
204
204
  const length = (array) => array?.length || 0
205
205
 
206
206
  /** Calculate the points for the number of threats at a particular level.
207
- * @arg {FoundThreats} from - the threats found for a certain player
207
+ * @arg {ColorThreats} from - the threats found for a certain player
208
208
  * @arg {number} level - the level to calculate the points for
209
209
  * @arg {number} [as] - if included, the level the points should be calculated as
210
210
  * @return {number} the number of points for the level
@@ -217,16 +217,23 @@ const attackScore = (from, level, as) => {
217
217
  }
218
218
 
219
219
  /** Shortcut to get the number of attacks.
220
- * @arg {FoundThreats} from - the threats found for a certain player
220
+ * @arg {ColorThreats} from - the threats found for a certain player
221
221
  * @returns {number} the number of attacks (win on next move) present
222
222
  */
223
223
  const attackCount = (from) => length(from['1'])
224
224
 
225
+ /** Shortcut to get the number of threats at a certain level.
226
+ * @arg {ColorThreats} from - the threats found for a player
227
+ * @arg {number} level - the level for the threats to look for
228
+ * @returns {number} the number of threats at the given level
229
+ */
230
+ const threatCount = (from, level) => length(from[level])
231
+
225
232
  /** Shortcut to get the number of passive threats
226
- * @arg {FoundThreats} from - the threats found for a player
233
+ * @arg {ColorThreats} from - the threats found for a player
227
234
  * @returns {number} the number of passive threats (win in 2 or more moves)
228
235
  */
229
- const threatCount = (from) => {
236
+ const passiveCount = (from) => {
230
237
  let count = 0
231
238
  for (const [depth, array] of Object.entries(from)) {
232
239
  count += int(depth) > 1 ? length(array) : 0
@@ -236,23 +243,29 @@ const threatCount = (from) => {
236
243
  }
237
244
 
238
245
  /** Shortcut to determine if we have an active attack.
239
- * @arg {FoundThreats} from - the threats found for a player
246
+ * @arg {ColorThreats} from - the threats found for a player
240
247
  * @returns {boolean}
241
248
  */
242
249
  const activeAttack = (from) =>
243
- attackCount(from) > 1 || (attackCount(from) === 1 && threatCount(from) > 0)
250
+ attackCount(from) > 1 || (attackCount(from) === 1 && passiveCount(from) > 0)
244
251
 
245
252
  /** Shortcut to determine if we have a passive attack.
246
- * @arg {FoundThreats} from - the threats found for a player
253
+ * @arg {ColorThreats} from - the threats found for a player
247
254
  * @returns {boolean}
248
255
  */
249
- const passiveAttack = (from) => threatCount(from) > 1
256
+ const passiveAttack = (from) => passiveCount(from) > 1
257
+
258
+ // /** Shortcut to determine if we are under attack.
259
+ // * @arg {ColorThreats} from - the threats found for a player
260
+ // * @returns {boolean}
261
+ // */
262
+ // const underAttack = (from) => attackCount(from) > 0
250
263
 
251
- /** Shortcut to determine if we are under attack.
252
- * @arg {FoundThreats} from - the threats found for a player
264
+ /** Shortcut to determine if we are under threat.
265
+ * @arg {ColorThreats} from - the threats found for a player
253
266
  * @returns {boolean}
254
267
  */
255
- const underAttack = (from) => attackCount(from) + threatCount(from) > 0
268
+ const underThreat = (from) => attackCount(from) + passiveCount(from) > 0
256
269
 
257
270
  // Analysis class
258
271
  export class Analysis {
@@ -282,6 +295,10 @@ export class Analysis {
282
295
  b: (level) => this.threats.b[level]?.length || 0,
283
296
  }
284
297
  this.#partial = Boolean(partial)
298
+ if (this.#moves.length === 0 || game.over) {
299
+ this.#edge = { b: '', w: '', rawEdge: [] }
300
+ this.#threats = { b: {}, w: {} }
301
+ }
285
302
  }
286
303
 
287
304
  get game() {
@@ -383,7 +400,7 @@ export class Analysis {
383
400
  const attacking = activeAttack(inactive)
384
401
 
385
402
  // Was the inactive player unable to clear all threats?
386
- const defending = !attacking && underAttack(active)
403
+ const defending = !attacking && underThreat(active)
387
404
 
388
405
  // Does the inactive player have multiple threats
389
406
  const threatening = !attacking && passiveAttack(inactive)
@@ -559,15 +576,17 @@ export class Analysis {
559
576
  * @arg {string} color - the player color
560
577
  * @returns {Point[]} - locations relevant to the threat
561
578
  */
562
- #threatLocs(threat, color) {
579
+ threatLocs(threat, color) {
563
580
  const { edge } = this.edge
564
581
  const max = edge.length
565
582
  let index = threat.at
583
+ let count = 0
566
584
  const locs = new Set([index])
567
- while (threat.match.length > locs.size) {
585
+ while (count < threat.match.length) {
568
586
  index += 1
569
587
  if (index >= max) index -= max
570
588
  locs.add(index)
589
+ count += 1
571
590
  // Check to see if we have a connectable pair
572
591
  if (edge[index]['p' + color]) {
573
592
  const line = 'n' + color
@@ -583,16 +602,15 @@ export class Analysis {
583
602
  }
584
603
  }
585
604
 
586
- const value = [...locs].map((i) => new Point(edge[i]))
587
- return value
605
+ return [...locs].map((i) => new Point(edge[i] || { x: 0, y: 0 }))
588
606
  }
589
607
 
590
608
  /** Apply the threats to a color's regex-ready edge.
591
609
  * @arg {Color} color - the color to find threats for
592
- * @returns {FoundThreats}
610
+ * @returns {ColorThreats}
593
611
  */
594
612
  #applyThreats(color) {
595
- /** @type {FoundThreats} */
613
+ /** @type {ColorThreats} */
596
614
  const matches = {}
597
615
  const { length } = this.edge[color]
598
616
  const edge = this.edge[color].repeat(2)
@@ -616,7 +634,7 @@ export class Analysis {
616
634
  /** Clean up the list of matches, removing any false positives. */
617
635
  #validateThreats() {
618
636
  for (const bw of ['w', 'b']) {
619
- /** @type FoundThreats */
637
+ /** @type ColorThreats */
620
638
  const matches = this.#threats[bw]
621
639
  for (const key of Object.keys(matches)) {
622
640
  const level = Number(key)
@@ -648,10 +666,12 @@ export class Analysis {
648
666
  * @returns {boolean}
649
667
  */
650
668
  #canActivateThreat(threat, color, level) {
651
- const attacks = this.#threats[color][1]?.length ?? 0
652
- const other = this.#threats[Trax.other(color)][1]?.length ?? 0
653
- const higher = this.#threats[color][level - 1]?.length ?? 0
654
- for (const loc of this.#threatLocs(threat, color)) {
669
+ const attacks = attackCount(this.#threats[color])
670
+ const player = Trax.playerNumber(color)
671
+ const other = Trax.other(color)
672
+ const otherAttacks = attackCount(this.#threats[other])
673
+ const higher = threatCount(this.#threats[color], level - 1)
674
+ for (const loc of this.threatLocs(threat, color)) {
655
675
  for (const type of this.game.possibleTiles(loc)) {
656
676
  const game = new Trax(this.game.rules, this.game.moves)
657
677
  const result = game.dropTile(type, loc)
@@ -659,20 +679,23 @@ export class Analysis {
659
679
 
660
680
  // This might be a bad shortcut, but if we already have an attack here
661
681
  // we just assume that this threat is also valid ...
662
- if (game.over && game.turn === Trax.playerNumber(color)) return true
682
+ if (game.over && game.turn === player) return true
683
+
684
+ // Otherwise, we cannot evaluate an ended game
685
+ if (game.over) continue
663
686
 
664
687
  const { threats } = analyze(game, true)
665
688
 
666
689
  // If we give our opponent an attack, this is a faulty move
667
- const faulty = (threats[Trax.other(color)][1]?.length ?? 0) - other
690
+ const faulty = attackCount(threats[other]) - otherAttacks
668
691
  if (faulty > 0) continue
669
692
 
670
693
  // Activating the threat means we get an attack and a threat
671
- const newAttacks = (threats[color][1]?.length ?? 0) - attacks
694
+ const newAttacks = attackCount(threats[color]) - attacks
672
695
  if (level === 2) {
673
696
  if (newAttacks > 1) return true
674
697
  } else {
675
- const newThreats = (threats[color][level - 1]?.length ?? 0) - higher
698
+ const newThreats = threatCount(threats[color], level - 1) - higher
676
699
  if (newAttacks > 0 && newThreats > 0) return true
677
700
  }
678
701
  }
@@ -766,7 +789,7 @@ export class Suggestion {
766
789
  }
767
790
  }
768
791
 
769
- export const suggest = (game) => {
792
+ export const suggest = (game, partial = false) => {
770
793
  const start = Date.now()
771
794
  const analyzed = {}
772
795
  const { color, moves, rules } = game
@@ -775,7 +798,7 @@ export const suggest = (game) => {
775
798
  trax.play(move)
776
799
  const unique = trax.normalized
777
800
  if (!analyzed[unique]) {
778
- const analysis = analyze(trax)
801
+ const analysis = analyze(trax, partial)
779
802
  analyzed[unique] = { move, score: analysis.scores[color], analysis }
780
803
  }
781
804
  }
@@ -248,7 +248,7 @@ const gameNotes = (move, notes) => {
248
248
  if (moveNotes.length === 0) return ''
249
249
  return (
250
250
  '; ' +
251
- moveNotes.map((n) => n.note.replaceAll('\n', '\n; ')).join('\n; ') +
251
+ moveNotes.map((n) => n.note.replace(/\n/g, '\n; ')).join('\n; ') +
252
252
  '\n'
253
253
  )
254
254
  }
@@ -328,8 +328,15 @@ exportCmd.fn = (CLI, id, filename) => {
328
328
  const source = sources[puzzle.src]
329
329
  content += '; Puzzle ' + game.puzzle
330
330
  if (source) {
331
- content += ' provided courtesy of ' + source.name
331
+ content += source.copyright
332
+ ? ' ©' + source.copyright + ' by '
333
+ : ' provided courtesy of '
334
+ content += source.name
332
335
  if (source.url) content += '\n; ' + source.url
336
+ if (source.license) {
337
+ content += '\n; Licensed under ' + source.license
338
+ content += source.licenseUrl ? ' ' + source.licenseUrl : ''
339
+ }
333
340
  }
334
341
 
335
342
  content += '\n'
package/src/cmds/notes.js CHANGED
@@ -25,6 +25,9 @@ notesCmd.fn = (CLI, id, ...note) => {
25
25
  if (id.startsWith('#')) id = id.slice(1)
26
26
 
27
27
  const game = CLI.GAMES[id]
28
+
29
+ if (!game) return CLI.error('Game not found.')
30
+
28
31
  const notes = game.notes || []
29
32
  const move = game.moves.length > 0 ? game.moves.split(' ').length : 0
30
33