@slugbugblue/trax 0.13.0 → 0.15.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/src/cmds/list.js CHANGED
@@ -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 || noun + 's')
45
-
46
43
  const alphaSort = (a, b) => {
47
44
  a = String(a).toLowerCase()
48
45
  b = String(b).toLowerCase()
@@ -104,6 +101,9 @@ listCmd.fn = (CLI, ...filters) => {
104
101
  return CLI.error('No games. Type "new" to start a new game.')
105
102
  }
106
103
 
104
+ const puzzle = filters.find((f) => f.length > 1 && 'puzzles'.startsWith(f))
105
+ if (puzzle) return CLI.do('puzzles', ...filters.filter((f) => f !== puzzle))
106
+
107
107
  let list = []
108
108
  const size = {
109
109
  id: 1,
@@ -120,7 +120,7 @@ listCmd.fn = (CLI, ...filters) => {
120
120
  id: (game.id === CLI.GAME.id ? '* #' : '#') + String(game.id),
121
121
  name: game.name?.length || 0,
122
122
  rules: game.rules,
123
- moves: plural(moves, 'move'),
123
+ moves: CLI.plural(moves, 'move'),
124
124
  smoves: game.over ? '9999' : String(moves).padStart(9, '0'),
125
125
  sturn: String(game.turn),
126
126
  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'],
@@ -98,6 +98,9 @@ newCmd.fn = (CLI, variant, ...names) => {
98
98
  let rules = ''
99
99
  if (variant) {
100
100
  variant = variant.toLowerCase()
101
+ if (variant.length > 1 && 'puzzles'.startsWith(variant)) {
102
+ return CLI.do('puzzles', 'new', ...names)
103
+ }
101
104
 
102
105
  if (['8x8', '8', 'trax8x8', '8trax', '8x8trax'].includes(variant)) {
103
106
  rules = 'trax8'
@@ -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,158 @@
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"] [level|puzzle]',
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 an = (word = '') => {
32
+ const a = /^[aeiou]/.test(word) ? 'an' : 'a'
33
+ return a + ' ' + word
34
+ }
35
+
36
+ const showPuzzle = (CLI, id) => {
37
+ const puzzle = puzzles.find((p) => p.id === id)
38
+ if (!puzzle) return CLI.error(`Puzzle ${id} not found`)
39
+ const trax = new Trax(puzzle.game, puzzle.notation, 'cli')
40
+ const players = ['white', 'black']
41
+ CLI.display(trax, players)
42
+ let out = CLI.color.short(puzzle.id)
43
+ out += CLI.color(` is ${an(levels[puzzle.level])} puzzle`) + '\n'
44
+ if (puzzle.title) out += CLI.color(puzzle.title) + '\n'
45
+ if (puzzle.desc) out += CLI.color.help(puzzle.desc) + '\n'
46
+ out += CLI.color(players[puzzle.player - 1] + ' to win by move ' + puzzle.max)
47
+ const src = sources[puzzle.src]
48
+ if (src) {
49
+ let source = '\nPuzzle '
50
+ source += src.copyright
51
+ ? 'copyright ' + src.copyright + ' by'
52
+ : 'courtesy of'
53
+ source += ' ' + src.name
54
+ const url = src.url ? ' ' + CLI.color.white(src.url) : ''
55
+ out += CLI.color.black(source) + url
56
+ }
57
+
58
+ CLI.out(out)
59
+ }
60
+
61
+ const listPuzzles = (CLI) => {
62
+ let size = 1
63
+ const data = []
64
+ for (const name of validLevels) {
65
+ size = Math.max(size, name.length)
66
+ data.push({ name, count: 0, solved: 0, selected: false })
67
+ }
68
+
69
+ if (data[CLI.puzzleLevel]) data[CLI.puzzleLevel].selected = true
70
+
71
+ for (const puzzle of puzzles) {
72
+ data[puzzle.level].count += 1
73
+ if (CLI.puzzle(puzzle.id).solved > 0) data[puzzle.level].solved += 1
74
+ }
75
+
76
+ for (const category of data) {
77
+ let out = category.selected ? CLI.color('* ') : ' '
78
+ out += CLI.color.short(category.name.padEnd(size + 1))
79
+ if (category.solved === 0) {
80
+ out += CLI.color(CLI.plural(category.count, 'puzzle'))
81
+ } else if (category.solved === category.count) {
82
+ if (category.count > 3) out += CLI.color.success('all ')
83
+ out += CLI.color.success(CLI.plural(category.count, 'puzzle') + ' solved')
84
+ } else {
85
+ out += CLI.color.success(`${category.solved} solved`)
86
+ out += CLI.color(` of ${category.count} puzzles`)
87
+ }
88
+
89
+ CLI.out(out)
90
+ }
91
+ }
92
+
93
+ const listLevel = (CLI, levelName) => {
94
+ let size = 1
95
+ const level = levels.indexOf(levelName)
96
+ const data = []
97
+ for (const puzzle of puzzles) {
98
+ if (puzzle.level !== level) continue
99
+ size = Math.max(size, puzzle.id.length)
100
+ data.push({
101
+ id: puzzle.id,
102
+ title: puzzle.title,
103
+ solved: CLI.puzzle(puzzle.id).solved > 0,
104
+ })
105
+ }
106
+
107
+ for (const puzzle of data) {
108
+ let out = CLI.color.success(puzzle.solved ? '✓' : ' ')
109
+ out += CLI.color.short(' ' + puzzle.id)
110
+ if (puzzle.title) out += CLI.color(' ' + puzzle.title)
111
+ CLI.out(out)
112
+ }
113
+ }
114
+
115
+ const startPuzzle = (CLI, puzzle) => {
116
+ const id = CLI.newPuzzle(puzzle)
117
+ CLI.out(CLI.color(`Started puzzle ${puzzle.id} as game #${id}`))
118
+ showPuzzle(CLI, puzzle.id)
119
+ }
120
+
121
+ const startNextPuzzle = (CLI, level) => {
122
+ if (level === undefined) level = CLI.puzzleLevel
123
+ const unsolved = puzzles.filter((p) => !CLI.puzzle(p.id).solved)
124
+ if (unsolved.length === 0) {
125
+ CLI.out(CLI.success('You have already completed all the puzzles.'))
126
+ return CLI.error('Specify a specific puzzle to try one again.')
127
+ }
128
+
129
+ const next = unsolved.find((p) => p.level >= level) || unsolved[0]
130
+ startPuzzle(CLI, next)
131
+ }
132
+
133
+ puzzlesCmd.fn = (CLI, action, id) => {
134
+ action = action?.toLowerCase() ?? 'list'
135
+ if ('start'.startsWith(action) || 'new'.startsWith(action)) {
136
+ if (!id) return startNextPuzzle(CLI)
137
+
138
+ const puzzle = puzzles.find((p) => p.id === id)
139
+ if (puzzle) {
140
+ return startPuzzle(CLI, puzzle)
141
+ }
142
+
143
+ if (validLevels.includes(id)) {
144
+ return startNextPuzzle(CLI, levels.indexOf(id))
145
+ }
146
+
147
+ return CLI.error('Could not start puzzle ' + id)
148
+ }
149
+
150
+ if (action) {
151
+ const x = id || action
152
+ if (validLevels.includes(x)) return listLevel(CLI, x)
153
+ if (puzzleIds.includes(x)) return showPuzzle(CLI, x)
154
+ if (id) return CLI.error('Could not find puzzle ' + id)
155
+ }
156
+
157
+ listPuzzles(CLI)
158
+ }
@@ -36,7 +36,7 @@ suggestCmd.fn = (CLI, debug) => {
36
36
 
37
37
  CLI.out(
38
38
  CLI.bubble(
39
- game.turn === '1' ? 'wh' : 'bh',
39
+ game.turn === 1 ? 'w' : 'b',
40
40
  String(game.move + 1) + '. ' + suggestion.pick.move,
41
41
  ),
42
42
  )
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
  }
package/src/puzzles.js CHANGED
@@ -4,10 +4,20 @@
4
4
  * @license Apache-2.0
5
5
  */
6
6
 
7
+ /** @type string[] */
8
+ export const levels = [
9
+ 'tutorial',
10
+ 'beginner',
11
+ 'tough',
12
+ 'advanced',
13
+ 'expert',
14
+ 'master',
15
+ ]
16
+
7
17
  /** @type {{[key: string]: PuzzleSource}} */
8
18
  export const sources = {
9
19
  sbb: {
10
- name: 'puzzlebot',
20
+ name: "slugbugblue's puzzlebot",
11
21
  },
12
22
  traxgame: {
13
23
  name: 'Donald G. Bailey',
@@ -25,8 +35,8 @@ export const puzzles = [
25
35
  icon: 'fd',
26
36
  player: 1,
27
37
  max: 3,
28
- level: 1,
29
- title: 'Tutorial: Loop attack',
38
+ level: 0,
39
+ title: 'Loop attack',
30
40
  desc: "You're playing white, so when you form a white loop, you win the game.",
31
41
  hints: ['A0/', 'B0\\'],
32
42
  },
@@ -38,8 +48,8 @@ export const puzzles = [
38
48
  icon: 'def',
39
49
  player: 2,
40
50
  max: 4,
41
- level: 1,
42
- title: 'Tutorial: Wide loop attack',
51
+ level: 0,
52
+ title: 'Wide loop attack',
43
53
  desc: "Your loop doesn't have to be a perfect circle, and it can be oriented in any direction.",
44
54
  hint: 'B2+',
45
55
  },
@@ -51,10 +61,10 @@ export const puzzles = [
51
61
  icon: 'ca:0e:0d',
52
62
  player: 1,
53
63
  max: 5,
54
- level: 1,
64
+ level: 0,
55
65
  title: 'Partially formed wide loop',
56
66
  desc: "It doesn't matter which direction the loops are facing.",
57
- hint: 'A2+',
67
+ hints: ['A2+', 'A3\\'],
58
68
  },
59
69
  {
60
70
  id: 'tutorial.04',
@@ -64,7 +74,7 @@ export const puzzles = [
64
74
  icon: 'd:aeec',
65
75
  player: 2,
66
76
  max: 6,
67
- level: 1,
77
+ level: 0,
68
78
  title: 'Using a hollow',
69
79
  desc: 'You can use forced tiles with an inside corner to stretch your attack.',
70
80
  hint: 'C1+',
@@ -77,7 +87,7 @@ export const puzzles = [
77
87
  icon: 'ca:0ef:0ec:0d',
78
88
  player: 1,
79
89
  max: 7,
80
- level: 1,
90
+ level: 0,
81
91
  title: 'Diverted attack',
82
92
  desc: "Even though black is attacking into this area, it doesn't stop you from completing your loop.",
83
93
  hint: 'A3+',
@@ -90,7 +100,7 @@ export const puzzles = [
90
100
  icon: 'd2f:aeeec',
91
101
  player: 2,
92
102
  max: 8,
93
- level: 1,
103
+ level: 0,
94
104
  title: 'Long cave',
95
105
  desc: 'Stretch the line with forced tiles.',
96
106
  hint: 'C1+',
@@ -103,7 +113,7 @@ export const puzzles = [
103
113
  icon: 'ae:0f:0a',
104
114
  player: 1,
105
115
  max: 7,
106
- level: 1,
116
+ level: 0,
107
117
  title: 'Introduction to the L',
108
118
  desc: 'Use an L to form two attacks at once.',
109
119
  hint: 'A3/',
@@ -116,7 +126,7 @@ export const puzzles = [
116
126
  icon: 'f:bae:1f',
117
127
  player: 2,
118
128
  max: 8,
119
- level: 1,
129
+ level: 0,
120
130
  title: 'Mirrored Ls',
121
131
  desc: "This mirrored position gives an L to both players, but since it's your turn, you have the initiative.",
122
132
  hint: 'A3/',
@@ -129,7 +139,7 @@ export const puzzles = [
129
139
  icon: 'a:efae',
130
140
  player: 2,
131
141
  max: 8,
132
- level: 1,
142
+ level: 0,
133
143
  title: 'Sideways L',
134
144
  desc: 'An L can be in any orientation.',
135
145
  hint: 'D1+',
@@ -142,7 +152,7 @@ export const puzzles = [
142
152
  icon: 'fae:1ef',
143
153
  player: 2,
144
154
  max: 8,
145
- level: 1,
155
+ level: 0,
146
156
  title: 'Ls come in many shapes',
147
157
  hint: 'A2/',
148
158
  },
@@ -150,13 +160,13 @@ export const puzzles = [
150
160
  id: 'tutorial.11',
151
161
  src: 'sbb',
152
162
  game: 'trax',
153
- notation: '@0+ @1\\ @1\\ D1+ D2\\',
163
+ notation: '@0+ B1/ C1/ @1+ A2/',
154
164
  icon: 'faee:2f',
155
165
  player: 2,
156
166
  max: 8,
157
- level: 1,
167
+ level: 0,
158
168
  title: 'Another L',
159
- hint: 'A2/',
169
+ hint: 'D2\\',
160
170
  },
161
171
  {
162
172
  id: 'tutorial.12',
@@ -166,7 +176,7 @@ export const puzzles = [
166
176
  icon: 'ae:fd:0ae',
167
177
  player: 2,
168
178
  max: 8,
169
- level: 1,
179
+ level: 0,
170
180
  title: 'Fat L',
171
181
  desc: "Even though this looks a bit odd, it's just another L.",
172
182
  hint: 'C1/',
@@ -179,7 +189,7 @@ export const puzzles = [
179
189
  icon: 'bbae:2f',
180
190
  player: 2,
181
191
  max: 8,
182
- level: 1,
192
+ level: 0,
183
193
  title: 'Connectable pair L',
184
194
  desc: 'This L uses a connectable pair instead of two corners, but it works the same as the other Ls.',
185
195
  hints: ['A2\\', 'B2/'],
@@ -188,14 +198,14 @@ export const puzzles = [
188
198
  id: 'tutorial.14',
189
199
  src: 'sbb',
190
200
  game: 'trax',
191
- notation: '@0+ @1\\ @1\\ C2+ C3+',
201
+ notation: '@0+ @1/ @1/ C0+ C0+',
192
202
  icon: 'fae:1e:1e',
193
203
  player: 2,
194
204
  max: 8,
195
- level: 1,
205
+ level: 0,
196
206
  title: 'Another connectable pair L',
197
207
  desc: 'There is a lot of variation in how Ls look, but being able to recognize them and activate them is key.',
198
- hint: 'A2/',
208
+ hint: 'A2\\',
199
209
  },
200
210
  {
201
211
  id: 'tutorial.15',
@@ -205,7 +215,7 @@ export const puzzles = [
205
215
  icon: 'e:e:d:ae',
206
216
  player: 2,
207
217
  max: 8,
208
- level: 1,
218
+ level: 0,
209
219
  title: 'Simple connectable pair L',
210
220
  desc: 'Starting to get the hang of this?',
211
221
  hints: ['B1\\', 'B2/'],
@@ -218,7 +228,7 @@ export const puzzles = [
218
228
  icon: 'a:e:eec',
219
229
  player: 2,
220
230
  max: 8,
221
- level: 1,
231
+ level: 0,
222
232
  title: 'Strange L',
223
233
  desc: 'Just another connectable pair L.',
224
234
  hint: 'B1/',
@@ -430,7 +440,7 @@ export const puzzles = [
430
440
  level: 2,
431
441
  player: 1,
432
442
  max: 11,
433
- hint: 'A0/',
443
+ hints: ['A0/', 'C4\\', 'D5/'],
434
444
  },
435
445
  {
436
446
  id: 'iw3.trx',
@@ -441,7 +451,7 @@ export const puzzles = [
441
451
  level: 2,
442
452
  player: 1,
443
453
  max: 11,
444
- hint: 'B0/',
454
+ hints: ['B0/', 'C0\\'],
445
455
  },
446
456
  {
447
457
  id: 'iw4.trx',
@@ -452,7 +462,7 @@ export const puzzles = [
452
462
  level: 2,
453
463
  player: 1,
454
464
  max: 11,
455
- hint: '@2/',
465
+ hints: ['@2/', 'C0/'],
456
466
  },
457
467
  {
458
468
  id: 'iw5.trx',
@@ -485,7 +495,7 @@ export const puzzles = [
485
495
  level: 2,
486
496
  player: 1,
487
497
  max: 13,
488
- hint: 'A1+',
498
+ hints: ['A1+', 'D1/'],
489
499
  },
490
500
  {
491
501
  id: 'ib1.trx',
@@ -507,7 +517,7 @@ export const puzzles = [
507
517
  level: 2,
508
518
  player: 2,
509
519
  max: 12,
510
- hint: 'A1\\',
520
+ hints: ['A1\\', 'E1/'],
511
521
  },
512
522
  {
513
523
  id: 'ib3.trx',
@@ -529,7 +539,7 @@ export const puzzles = [
529
539
  level: 2,
530
540
  player: 2,
531
541
  max: 12,
532
- hint: 'B0/',
542
+ hints: ['B0/', 'C0+'],
533
543
  },
534
544
  {
535
545
  id: 'ib5.trx',
@@ -562,7 +572,7 @@ export const puzzles = [
562
572
  level: 2,
563
573
  player: 2,
564
574
  max: 10,
565
- hint: 'A0/',
575
+ hints: ['A0/', 'D3/'],
566
576
  },
567
577
  {
568
578
  id: 'ib8.trx',
@@ -638,8 +648,8 @@ export const puzzles = [
638
648
  icon: '0ca:deef:0d',
639
649
  level: 2,
640
650
  player: 1,
641
- max: 13,
642
- hint: 'B0/',
651
+ max: 15,
652
+ hints: ['B0/', 'C0\\'],
643
653
  },
644
654
  {
645
655
  id: 'hw4.trx',
@@ -737,8 +747,8 @@ export const puzzles = [
737
747
  icon: 'd:b:ba:aeecd:0fdec',
738
748
  level: 1,
739
749
  player: 2,
740
- max: 16,
741
- hint: 'B1+',
750
+ max: 18,
751
+ hints: ['B1+', 'B2/', 'D3+'],
742
752
  },
743
753
  {
744
754
  id: 'hb3.trx',
@@ -757,7 +767,7 @@ export const puzzles = [
757
767
  game: 'traxloop',
758
768
  notation: '@0+ A0/ A0/ A4/ A5+ @5/ @5/ @5+ A4/ @5/',
759
769
  icon: '3d:3c:3e:0d1d:cbdcb',
760
- level: 2,
770
+ level: 3,
761
771
  player: 1,
762
772
  max: 15,
763
773
  hint: 'D1\\',
@@ -768,7 +778,7 @@ export const puzzles = [
768
778
  game: 'traxloop',
769
779
  notation: '@0+ B1/ C1+ C0/ D1\\ E1\\ F1+ E0/ F1/',
770
780
  icon: '2cdc:1dfae:ecb',
771
- level: 2,
781
+ level: 3,
772
782
  player: 2,
773
783
  max: 16,
774
784
  hint: 'F0/',
@@ -779,7 +789,7 @@ export const puzzles = [
779
789
  game: 'traxloop',
780
790
  notation: '@0/ @1+ A0/ A0/ B2+ @2\\ @2\\ E3\\ E4+',
781
791
  icon: '1ca:afde:1bdf:3b',
782
- level: 2,
792
+ level: 3,
783
793
  player: 2,
784
794
  max: 16,
785
795
  hint: 'A1\\',
@@ -790,9 +800,13 @@ export const puzzles = [
790
800
  game: 'traxloop',
791
801
  notation: '@0/ B1+ C1/ C0/ C0\\ @3/ C1+ E2/ F2\\',
792
802
  icon: '1baf:1cdca:cdecd',
793
- level: 2,
803
+ level: 3,
794
804
  player: 2,
795
805
  max: 16,
796
806
  hint: 'B1+',
797
807
  },
798
808
  ]
809
+
810
+ puzzles.sort(
811
+ (a, b) => a.level - b.level || a.max - b.max || (a.id < b.id ? -1 : 1),
812
+ )