Closes#1575
Implements structure recognition.
## Features
* Structure browsing dialog (`F6`) that becomes available if a scenario declares any recognizable structures
* Automatically recognizes statically-placed structures upon scenario initialization, accounting for occlusion by other entity/structure placement
* New `structure` command for querying location of recognized structures (primarily intended for system robots and goal checking)
* Efficiently recognizes structures immediately upon completion
* Accounts for removal of structures
* Several new integration tests
* Structured web-interface log to help understand/debug the recognition process
* Re-uses much of the functionality built previously for defining structures (#1332)
Other changes:
* Improved validation for static structure placement (ensure valid structure names instead of failing silently)
* Moved a few functions (`getNeighborLocs`, `zoomWorld`, `entityAt`, `robotWithID`, `robotWithName`) out of `Step.Util` and into `State` so that recognizer initialization, which becomes a field in `GameState`, could use them
* split `scenarioToGameState` into pure and non-pure functions
## Optimizations
Scenarios that do not make use of structure recognition are entirely unaffected, performance-wise.
Some optimizations include:
* Structure definitions must "opt-in" to participate in automatic recognition
* Aho-Corasick automatons optimized by:
* only initiate structure search if a placed entity exists on a participating structure template
* initializing different automatons for each type of "placed entity"
* pruning inapplicable row candidates for 2-D search
The row-level structure recognition cache described in #1575 was not implemented; it's probably not worth the complexity cost.
# UI Demo
scripts/play.sh -i scenarios/Testing/1575-structure-recognizer/1575-browse-structures.yaml --autoplay
1. Press `F6` for Structure Browser dialog
2. View http://localhost:5357/recognize/log and http://localhost:5357/recognize/found
![image](https://github.com/swarm-game/swarm/assets/261693/e32d3572-7e53-42d6-84cd-393c57a8aeac)
# Future improvements
* Refactor `State.hs` so that the new helper functions can live elsewhere
* Support non-rectangular recognizable structures
* Allow flip/rotate of recognizable structures
* Structure ownership by robots
* Consolidate code between the Goals and Structures modal dialogs, and the Achievements browser
* Enforce minimum/maximum dimensions for structure definitions
This allows the validation to work in VS Code if one happens to open the symlinked version of a scenario file, which is easy to do accidentally via CTRL+P.
* add wave program and parametrise it to compare inlined/generic version
* use [`tasty-bench`](https://hackage.haskell.org/package/tasty-bench) library to show comparison
* move benchmarks to test folder as they can now share tasty code
* closes#1574
Using the recursive definition with ifs leads to a 3x slowdown:
```
wavesInlined
10: OK
361 ms ± 29 ms
20: OK
718 ms ± 35 ms
30: OK
1.066 s ± 28 ms
40: OK
1.437 s ± 37 ms
wavesWithDef
10: OK
1.052 s ± 51 ms, 2.92x
20: OK
2.117 s ± 34 ms, 2.95x
30: OK
3.144 s ± 80 ms, 2.95x
40: OK
4.191 s ± 91 ms, 2.92x
```
But if we just inline and simplify the code, we can remove the runtime overhead completely.
Closes#1436.
The schema `.json` files are now the authoritative source of truth for documentation.
Wrote a very simple parser for JsonSchema to extract the documentation from JSON.
Split the README.md into [static](c314cc50a1/data/scenarios/README.md) and [auto-generated](c314cc50a1/data/scenarios/doc-fragments/SCHEMA.md) parts.
Added a custom `"footers"` key to schema files to support inclusion of other markdown files for each object description.
# Schema doc regeneration
./scripts/regenerate-schema-docs.sh
Towards #1494.
Replaced/restricted uses of `Prelude.tail` and `Prelude.!!`. Quarantined `undefined`.
Introduced a new function `listEnumsNonempty` that is guaranteed safe.
Should benchmark this to see if it's faster than `Integer`.
# `stack bench` output
Personally, I'm not sure what the best way to benchmark this particular change, but here's the output of `stack bench` anyway:
## Before
```
benchmarking run 1000 game ticks/idlers/10
time 2.634 ms (2.629 ms .. 2.641 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 2.636 ms (2.634 ms .. 2.640 ms)
std dev 10.45 μs (6.667 μs .. 15.88 μs)
benchmarking run 1000 game ticks/idlers/20
time 2.660 ms (2.653 ms .. 2.668 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 2.661 ms (2.657 ms .. 2.667 ms)
std dev 18.53 μs (12.24 μs .. 30.77 μs)
benchmarking run 1000 game ticks/idlers/30
time 2.660 ms (2.653 ms .. 2.670 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 2.685 ms (2.678 ms .. 2.696 ms)
std dev 29.12 μs (20.64 μs .. 42.37 μs)
benchmarking run 1000 game ticks/idlers/40
time 2.693 ms (2.686 ms .. 2.700 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 2.694 ms (2.689 ms .. 2.705 ms)
std dev 26.61 μs (17.81 μs .. 45.78 μs)
benchmarking run 1000 game ticks/trees/10
time 2.627 ms (2.623 ms .. 2.632 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 2.625 ms (2.622 ms .. 2.629 ms)
std dev 12.55 μs (8.749 μs .. 18.38 μs)
benchmarking run 1000 game ticks/trees/20
time 2.647 ms (2.642 ms .. 2.653 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 2.648 ms (2.645 ms .. 2.656 ms)
std dev 17.86 μs (10.58 μs .. 31.07 μs)
benchmarking run 1000 game ticks/trees/30
time 2.672 ms (2.667 ms .. 2.676 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 2.673 ms (2.668 ms .. 2.683 ms)
std dev 22.85 μs (14.67 μs .. 38.46 μs)
benchmarking run 1000 game ticks/trees/40
time 2.697 ms (2.693 ms .. 2.700 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 2.699 ms (2.694 ms .. 2.708 ms)
std dev 23.96 μs (16.47 μs .. 38.01 μs)
benchmarking run 1000 game ticks/circlers/10
time 229.9 ms (229.0 ms .. 230.8 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 230.3 ms (230.0 ms .. 230.8 ms)
std dev 639.7 μs (458.4 μs .. 842.9 μs)
variance introduced by outliers: 11% (moderately inflated)
benchmarking run 1000 game ticks/circlers/20
time 434.7 ms (433.9 ms .. 435.2 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 435.7 ms (435.2 ms .. 436.5 ms)
std dev 800.9 μs (432.2 μs .. 1.072 ms)
variance introduced by outliers: 14% (moderately inflated)
benchmarking run 1000 game ticks/circlers/30
time 637.5 ms (634.5 ms .. 641.2 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 640.5 ms (639.2 ms .. 641.6 ms)
std dev 1.553 ms (1.244 ms .. 1.907 ms)
variance introduced by outliers: 16% (moderately inflated)
benchmarking run 1000 game ticks/circlers/40
time 851.5 ms (NaN s .. 864.0 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 852.1 ms (850.0 ms .. 853.4 ms)
std dev 2.138 ms (657.7 μs .. 2.908 ms)
variance introduced by outliers: 19% (moderately inflated)
benchmarking run 1000 game ticks/movers/10
time 382.4 ms (379.9 ms .. 384.1 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 383.9 ms (382.8 ms .. 385.1 ms)
std dev 1.601 ms (973.2 μs .. 2.420 ms)
variance introduced by outliers: 14% (moderately inflated)
benchmarking run 1000 game ticks/movers/20
time 721.9 ms (712.2 ms .. 729.8 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 722.6 ms (720.4 ms .. 725.9 ms)
std dev 3.234 ms (1.128 ms .. 4.250 ms)
variance introduced by outliers: 19% (moderately inflated)
benchmarking run 1000 game ticks/movers/30
time 1.076 s (1.063 s .. 1.095 s)
1.000 R² (1.000 R² .. 1.000 R²)
mean 1.071 s (1.067 s .. 1.074 s)
std dev 4.317 ms (2.285 ms .. 5.464 ms)
variance introduced by outliers: 19% (moderately inflated)
benchmarking run 1000 game ticks/movers/40
time 1.440 s (1.436 s .. 1.446 s)
1.000 R² (NaN R² .. 1.000 R²)
mean 1.436 s (1.435 s .. 1.438 s)
std dev 1.923 ms (544.1 μs .. 2.572 ms)
variance introduced by outliers: 19% (moderately inflated)
```
## After
```
benchmarking run 1000 game ticks/idlers/10
time 2.559 ms (2.515 ms .. 2.626 ms)
0.996 R² (0.994 R² .. 0.999 R²)
mean 2.557 ms (2.539 ms .. 2.583 ms)
std dev 83.13 μs (63.17 μs .. 120.2 μs)
variance introduced by outliers: 20% (moderately inflated)
benchmarking run 1000 game ticks/idlers/20
time 2.518 ms (2.508 ms .. 2.528 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 2.534 ms (2.526 ms .. 2.546 ms)
std dev 36.80 μs (30.33 μs .. 51.03 μs)
benchmarking run 1000 game ticks/idlers/30
time 2.544 ms (2.527 ms .. 2.565 ms)
0.999 R² (0.999 R² .. 1.000 R²)
mean 2.540 ms (2.533 ms .. 2.551 ms)
std dev 35.25 μs (23.47 μs .. 54.64 μs)
benchmarking run 1000 game ticks/idlers/40
time 2.598 ms (2.578 ms .. 2.627 ms)
0.997 R² (0.995 R² .. 0.999 R²)
mean 2.693 ms (2.660 ms .. 2.733 ms)
std dev 135.0 μs (110.1 μs .. 169.4 μs)
variance introduced by outliers: 38% (moderately inflated)
benchmarking run 1000 game ticks/trees/10
time 2.590 ms (2.538 ms .. 2.642 ms)
0.998 R² (0.998 R² .. 0.999 R²)
mean 2.548 ms (2.538 ms .. 2.566 ms)
std dev 49.94 μs (39.27 μs .. 67.19 μs)
benchmarking run 1000 game ticks/trees/20
time 2.617 ms (2.568 ms .. 2.679 ms)
0.998 R² (0.997 R² .. 0.999 R²)
mean 2.562 ms (2.541 ms .. 2.585 ms)
std dev 78.13 μs (63.71 μs .. 95.38 μs)
variance introduced by outliers: 19% (moderately inflated)
benchmarking run 1000 game ticks/trees/30
time 2.559 ms (2.533 ms .. 2.591 ms)
0.999 R² (0.999 R² .. 1.000 R²)
mean 2.555 ms (2.544 ms .. 2.573 ms)
std dev 49.00 μs (34.54 μs .. 65.61 μs)
benchmarking run 1000 game ticks/trees/40
time 2.820 ms (2.729 ms .. 2.906 ms)
0.994 R² (0.991 R² .. 0.996 R²)
mean 2.620 ms (2.594 ms .. 2.666 ms)
std dev 121.2 μs (86.30 μs .. 167.3 μs)
variance introduced by outliers: 34% (moderately inflated)
benchmarking run 1000 game ticks/circlers/10
time 230.6 ms (228.8 ms .. 232.5 ms)
1.000 R² (0.999 R² .. 1.000 R²)
mean 233.5 ms (231.7 ms .. 236.0 ms)
std dev 3.275 ms (1.853 ms .. 5.093 ms)
variance introduced by outliers: 11% (moderately inflated)
benchmarking run 1000 game ticks/circlers/20
time 448.8 ms (433.6 ms .. 464.6 ms)
0.999 R² (0.999 R² .. 1.000 R²)
mean 441.1 ms (437.3 ms .. 446.1 ms)
std dev 5.991 ms (3.879 ms .. 8.282 ms)
variance introduced by outliers: 14% (moderately inflated)
benchmarking run 1000 game ticks/circlers/30
time 655.5 ms (646.9 ms .. 666.9 ms)
1.000 R² (NaN R² .. 1.000 R²)
mean 652.3 ms (650.6 ms .. 654.5 ms)
std dev 2.448 ms (1.442 ms .. 3.702 ms)
variance introduced by outliers: 16% (moderately inflated)
benchmarking run 1000 game ticks/circlers/40
time 863.8 ms (861.6 ms .. 868.3 ms)
1.000 R² (1.000 R² .. NaN R²)
mean 865.5 ms (864.4 ms .. 867.3 ms)
std dev 1.833 ms (84.64 μs .. 2.435 ms)
variance introduced by outliers: 19% (moderately inflated)
benchmarking run 1000 game ticks/movers/10
time 397.4 ms (382.4 ms .. 412.7 ms)
0.999 R² (0.996 R² .. 1.000 R²)
mean 395.7 ms (391.2 ms .. 400.5 ms)
std dev 6.397 ms (4.350 ms .. 8.877 ms)
variance introduced by outliers: 14% (moderately inflated)
benchmarking run 1000 game ticks/movers/20
time 721.3 ms (698.4 ms .. 742.5 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 722.5 ms (719.8 ms .. 725.4 ms)
std dev 3.533 ms (1.527 ms .. 4.427 ms)
variance introduced by outliers: 19% (moderately inflated)
benchmarking run 1000 game ticks/movers/30
time 1.053 s (1.014 s .. 1.114 s)
1.000 R² (0.999 R² .. 1.000 R²)
mean 1.067 s (1.059 s .. 1.081 s)
std dev 13.55 ms (1.042 ms .. 17.01 ms)
variance introduced by outliers: 19% (moderately inflated)
benchmarking run 1000 game ticks/movers/40
time 1.392 s (1.333 s .. 1.421 s)
1.000 R² (1.000 R² .. 1.000 R²)
mean 1.439 s (1.417 s .. 1.481 s)
std dev 42.42 ms (222.2 μs .. 49.25 ms)
variance introduced by outliers: 19% (moderately inflated)
```
Extension of #1484 that partially mitigates #1558.
Let's not bother to track the activity levels of system robots when we're not in creative mode, because they won't even show up in the `F2` dialog. This mitigation can be substantial, as we generally expect there to be many more system robots than player robots.
It doesn't have quite the same "warm up" drawback as the potential mitigation described in https://github.com/swarm-game/swarm/issues/1558#issuecomment-1741794123, because player robots will always be warmed up, and system robots will have been warmed up if we've been playing in creative mode. We wouldn't necessarily expect frequent toggling in-and-out of creative mode while trying to observe performance in the `F2` dialog.
* generate Web API endpoint docs in CLI - `swarm generate endpoints`
* use default port in API docs using `renderCurlBasePath`
* fix code rendering in Web API so that the tree nodes try to fit one line
* try adding some API samples
* hand craft `ToJSON Robot` instance to match `FromJSONE` more closely
* default values are skipped
* inventory and devices are shortened to names and counts
Toward #1547.
I decided to simply fix the error in this PR. At #1547 there is discussion of various other means to prevent this kind of error in the future (both for developers and players), but I will leave implementing some of those ideas to a future PR.
Closes#353
Also adds a new top-level command to render a scenario map to the console.
Most of the work for this feature entailed identifying the subset of `GameState` that is actually needed for rendering the world, so that the required information can be retrieved from just the `Scenario` rather than having to instantiate an entire `GameState`.
# Potential follow-ups
- [ ] There is some noticeable lag when using the up/down arrow to navigate to any of the largely "procedurally generated" scenarios, e.g. `classic` or `creative`. May want to do caching of some kind. The other "challenge scenarios" render without perceptible delay.
- [ ] The heuristic for choosing the view center could be improved, possibly by defining new "hints" as part of the scenario schema.
- [ ] Rendering to the console could be augmented with color.
- [ ] Could render to other image formats like PNG or SVG
- [ ] account for a user-selected seed in the scenario launch parameters dialog
# Demos
## Scenario selection preview
![image](https://github.com/swarm-game/swarm/assets/261693/7c54c6bb-fb02-461f-98a1-06eccbbfc450)
## Command-line map rendering
```
~/github/swarm$ scripts/play.sh map data/scenarios/Challenges/ice-cream.yaml
OO
▒▒▒ ▒▒▒▒ OOOO
┌─┐▒┌──┐ MMMM
│B V6│ \ZZ/
└──────┘ \/
```
and
```
stack run -- map data/scenarios/Challenges/hackman.yaml
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒••••••••••▒••••••••••▒
▒o▒▒▒•▒▒▒▒•▒•▒▒▒▒•▒▒▒o▒
▒•▒▒▒•▒▒▒▒•▒•▒▒▒▒•▒▒▒•▒
▒•••••••••••••••••••••▒
▒•▒▒▒•▒•▒▒▒▒▒▒▒•▒•▒▒▒•▒
▒•••••▒••••▒••••▒•••••▒
▒▒▒▒▒•▒▒▒▒ ▒ ▒▒▒▒•▒▒▒▒▒
▒•▒ ▒•▒
▒▒▒▒▒•▒ ┌──=──┐ ▒•▒▒▒▒▒
• │ │ •
▒▒▒▒▒•▒ └─────┘ ▒•▒▒▒▒▒
▒•▒ ▒•▒
▒▒▒▒▒•▒ ▒▒▒▒▒▒▒ ▒•▒▒▒▒▒
▒••••••••••▒••••••••••▒
▒•▒▒▒•▒▒▒▒•▒•▒▒▒▒•▒▒▒•▒
▒o••▒•••••••••••••▒••o▒
▒▒▒•▒•▒•▒▒▒▒▒▒▒•▒•▒•▒▒▒
▒•••••▒••••▒••••▒•••••▒
▒•▒▒▒▒▒▒▒▒•▒•▒▒▒▒▒▒▒▒•▒
▒•••••••••••••••••••••▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
```
In preparation for #1483. `LogEntry` started life as something specific to robot logs. It then evolved to be used in the system log as well (see #1039 and #652), but in a sort of hacky way. This PR refactors `LogEntry` to be more generic.
- Move `Swarm.Game.Log` -> `Swarm.Log` since it's not specific to gameplay.
- Rename `ErrorLevel` to `Severity`, add a new `Info` level, and add a top-level `leSeverity` field
- Rename `leRobotName` to just `leName`, since it was already being used to name both robots and system components anyway
- Move robot-specific fields (*e.g.* robot ID) into the new `RobotLogSource` type, and add `LogSource` to differentiate between robot and system logs
- Various other minor improvements and tweaks
This adds two files. The first is the syntax file responsible for basic highlighting. The highlight categories are based roughly on those seen in the Emacs file ([swarm-mode.el](https://github.com/swarm-game/swarm/blob/main/editors/emacs/swarm-mode.el)). The second is the file for configuring the language server. Since it is based on Neovim's native LSP client, it only works with Neovim and not Vim.
`README.md` in the `editor` folder has also been updated to include instructions for setting these up.
Towards #872
Previously, the `GameState` record had `41` toplevel members. It now has `22`. Logical grouping of the fields makes it easier to peruse the code and paves the way to split `State.hs` into smaller modules. Some functions now may even be able to operate exclusively on subsets of the game state, rather than having to pass in `GameState` as a whole.
There is potential to go even farther, by extracting view-related and robot-related members to their own records, but I figured I'd pursue this refactoring incrementally.
Towards #1494
* Totally eliminates partial `head`
* introduces an hlint rule to ban unsafe use of `head`
* restricts use of the partial `fromList` from `NonEmpty`
Closes#351.
I sat down with @ccasin today to look at #351. He convinced me that it is actually fine to return `forall a. a -> a` as the type of `r` in `r <- return (\i.i)`, even though `return (\i.i)` has type `forall a. cmd (a -> a)` and we can never have a type `cmd (forall a. a -> a)`. Basically, in a Hindley-Milner-style system it is always safe to generalize at any time.
The real problem --- which I was very close to discovering in https://github.com/swarm-game/swarm/issues/351#issuecomment-1146274801 --- is simply that we need to generalize the type of variables bound by `<-`. However, we need to make sure to do so before putting that variable in the context, which I apparently failed to do when I tried fixing this before.
Closes#1461. Errors which used to be displayed in a pop-up window (parse errors, type errors) are now displayed inline in the REPL window instead.
- Get rid of the pop-up error dialog
- Also fix a bug introduced in #1481 where the REPL history would not be properly cleared when first starting a new scenario, because the old cache was still being used
- Add scrollbars on both the inventory and info panels
- Get rid of `. . .` at top and bottom of info panel, since we now have scrollbar as a visual indicator when there is more content
- Allow scrolling the REPL history (closes#60)
- PgUp/PgDown can be used to scroll (Shift+PgUp/Dn were not recognized on my system)
- Hitting any other key causes the view to jump back to the very bottom
- A computation finishing + printing an output also causes the view to jump to the bottom
- The REPL history is cached so that it only gets re-rendered whenever a new history entry (i.e. input or output) is added; this is needed since the history could get quite large.
- Also, fix the height of the key hint menus to 2 lines, even when the panel-specific menu (second line) is blank, so the world panel does not keep resizing as we move the focus between panels.
Thanks to @jtdaugherty for releasing `brick-1.10` with a new ability to specify blank space to the side of scrollbars; see https://github.com/jtdaugherty/brick/discussions/484 .
Also towards #1461 .