Towards #1797
Combines `TerrainEntityMaps` and `WorldMap` into a single record, `ScenarioInputs`.
Establishes a Matroska-style arrangement of records to represent various scopes of state, eliminating several redundant fields.
Makes use of rotation-aware structure recognition (#1678) to simplify goal checking for word search (#999).
![Screenshot from 2024-03-13 18-48-53](https://github.com/swarm-game/swarm/assets/261693/6fc9e62e-d9e2-4a13-8969-32dbee695e0a)
# Benchmarks
Using the following command (GHC 9.6.4):
scripts/run-tests.sh --test-arguments '--pattern "word-search"'
| Before | After |
| --- | --- |
| `0.98s` | `0.82s` |
This improves the test introduced in #1736 in a few ways:
* Place the system robot's code inline in the `program` field
* even though the system bot had been using `instant`, the `run` command itself incurs a delay. So this ensures the system robot is ready to monitor the cell immediately
* System robot uses exclusively `Intangible` commands in its monitoring loop
* This allows removal of the `instant` command.
* `Surveil`, which does not appear in any scenario yet, is made `Intangible`.
* Remove the initial `wait 2` in the player robot's solution.
Tested to ensure that this still exercises the problem that #1736 set out to solve, by commenting out this line (causing the scenario to fail).
01ae0e45d7/src/swarm-engine/Swarm/Game/State/Robot.hs (L396)
## Demo
scripts/play.sh -i data/scenarios/Testing/1598-detect-entity-change.yaml --autoplay --speed 1
Fixes#1598
## Demo
Illustrates immediate (same-tick) reaction to a change of a `watch`ed cell:
scripts/play.sh -i data/scenarios/Testing/1598-detect-entity-change.yaml --autoplay --speed 1
## Background
Robots can be **scheduled** to wakeup from a `watch` by the `wakeWatchingRobots` function.
Robots may be **actually awakened** by the `wakeUpRobotsDoneSleeping` function.
Previously, `wakeWatchingRobots` would only ever schedule wakeups at `currentTick + 1`. Then, once the next tick is reached, in `wakeUpRobotsDoneSleeping` only robots that had been scheduled to wake on precisely the **current tick** could actually be awakened.
But this made it impossible for one robot to cause another robot, which had been sleeping, to actually wake up within the same tick that scheduling of the wakeup is performed.
The importance of this comes into play for system robots that are `watch`ing a cell and intended to react instantaneously to player actions (usually by running code in an `instant` block).
During each "tick", every active robot gets exactly one "turn" to execute code. Given the following ID assignments:
| Robot ID | Robot name |
| --- | --- |
| `0` | `base` |
| `1` | `systemBot` |
the `systemBot` will always execute *after* the `base` robot in a given tick.
If the `systemBot` is `watch`ing a given cell, a modification of that cell by the `base` will schedule a wakeup of the `systemBot`. If the scheduled wakeup tick is `currentTick + 1`, then the `base` will have **another execution turn** before the `systemBot` gets an opportunity to react to the `base`'s action at the current tick, causing the `systemBot` to overlook actions that may be relevant to a goal condition.
## Solution
In contast, if we schedule the `systemBot` wakeup for `currentTick` instead of `currentTick + 1`, then it should have an opportunity to wake, run, and react to the `base`'s action before the `base`'s next turn.
But in the status quo, the selection of which robots to run in a given tick is made only once, before iteration over those robots begins. We need instead to update the robots-to-run pool dynamically, after each iteration on an individual robot. The most appropriate data structure for the iteration pool is probably a [Monotone priority queue](https://en.wikipedia.org/wiki/Monotone_priority_queue), but we approximate this with an `IntSet` of `RID`s and the [`minView`](https://hackage.haskell.org/package/containers-0.7/docs/Data-IntSet.html#v:minView) function.
Being able to alter the list of active robots in the middle of a given tick's robot iteration has required a change to the `runRobotIDs` function. Instead of using a `forM_` to iterate over a static list of `RIDs`, we have a custom iteration function `iterateRobots` to handle the dynamic set.
## Performance
Benchmarks were performed locally with `stack` and GHC 9.6.
I had tested the replacement of the `forM_` loop with `iterateRobots` in isolation, and it had zero effect on benchmarks. But some other trivial changes tested in isolation, such as the addition of docstrings, yielded a +6% increase in benchmarks. So there seems to be some instability in the ability of the compiler to perform certain optimizations.
With this PR, increases in benchmark times ranging from 7% to 11% were observed. The obvious culprit is that `wakeUpRobotsDoneSleeping` is now called N times per tick, rather than once per tick, where N is the number of robots that are initially (or become) active during that tick.
### Mitigating with an "awakened" set
In the common case (i.e. when there are *no* watching robots), the `wakeUpRobotsDoneSleeping` that is now executed `N` times per tick (instead of once per tick) incurs a `Map` lookup, which is `O(log M)` in the number of distinct future wakeup times across all robots.
However, those extra `N` invocations only exist to serve the purpose of `watch`ing robots that may need to wake up at the current tick. It may be more efficient to have a dedicated `Set` in the `GameState` for such robots that gets populated parallel to this insertion:
ad5c58917e/src/swarm-engine/Swarm/Game/State/Robot.hs (L387)
Then if this set remains `null`, we can avoid paying for an additional `O(N * log M)` operations entailed by the `Map` lookups into `internalWaitingRobots`.
#### Result
Indeed, storing awakened bots in a new `currentTickWakeableBots` set restored the benchmarks nearly to the baseline. Instead of the regressions in the 10-20% range observed before, now only a few benchmarks had at most 3-4% increases.
### CI regressions
In CI, (`snake`, `powerset`, and `word-search`) exceeded their timeout thresholds in GHC 9.2 and GHC 9.4. No regressions were observed with GHC 9.6. To accommodate the lowest common denominator, I bumped the thresholds for those three scenarios to get a passing build.
I don't believe it is worth further effort or investigation to optimize for GHC 9.4, since such efforts will be moot for GHC 9.6 and onward.
Test invocation:
cabal test swarm-integration --test-show-details streaming --test-options '--color always --pattern "word-search"'
| **Scenario** | GHC 9.4.8 | GHC 9.4.8 | GHC 9.6.4 | GHC 9.6.4 |
| --- | --- | --- | --- | --- |
| | Before | **After** | Before | **After**
| `snake` | 1.84s | **5.38s** | 1.62s | **1.67s** |
| `powerset` | 1.66s | **5.09s** | 1.56s | **1.66s** |
| `word-search` | 0.56s | **1.91s** | 0.44s | **0.48s** |
### Potential improvements
#### `TickNumber` as `Map` key
`waitingRobots` is of type `Map TickNumber [RID]`. Instead of `Map`, we could have a `newtype` that encapsulates a `IntMap` to make lookups by `TickNumber` more efficient.
A walkability "blacklist" had already been implemented in #1536. The whitelist shall **only** permit walking on the specified entities; blank cells become unwalkable.
This feature would allow use of the `path` command to check for "connectivity" by way of a "trail" of entities.
## Use cases
* system robots or goal conditions can use this to check whether the player has connected two points with a road, cable, aqueduct, etc.
* monkeys, which can only move along `tree`s
* sea life, which should only be able to move in `water`
## Testing
scripts/run-tests.sh --test-arguments '--pattern "walkable-entities"'
Closes#1641
The `data/terrain.yaml` file is now the authoritative source of terrains, though `BlankT` is still a hard-coded special case.
I have not changed the underlying integer representation of terrain in the world function, which means that the `terrainIndexByName` Map in the `TerrainMap` record is needed for translating between `Int` and `TerrainType`.
# Demo
scripts/play.sh -i data/scenarios/Testing/1775-custom-terrain.yaml
![Screenshot from 2024-02-22 16-51-53](https://github.com/swarm-game/swarm/assets/261693/1d263c8b-4e9c-40bf-bdc8-bf5ba8e33c4d)
# Changes
* There used to be a function called `integrateScenarioEntities` that combined the `EntityMap` stored in the `Scenario` record with the global entity map. However, the global entity map is accessible at parse time of the `Scenario`, so we do the combining there and only ever store the combined map in the `Scenario` record.
* JSON Schema for terrain
* Removed the distinction between "World" attributes and "Terrain" attributes
* Unit tests for scenario-defined terrain and related validations
* Validate existence of referenced terrain at scenario parse time
* Validate attributes referenced by terrains at parse time
Introduced 3 new sub-records in `UI.hs`:
* `UIGameplay`
* `UITiming`
* `UIInventory`
This allows some functions (e.g. `populateInventoryList`) to operate on a narrower scope of state.
Also uses `Brick.zoom` in a few more places to mitigate the longer lens chains.
Update to use the `lsp-2.4` API. Closes#1350.
Initially I hoped that any `lsp-2.x` would work. However, the `SeverDefinition` record changed in `2.2` so I initially set that as a lower bound. But then it turns out that `2.4` changed which module it is importing `Rope` from; since we work with ropes in the `Hover` module it matters since we have to import the matching module. Updating to the new `Rope` type also required some changes as the API provided by the new `Rope.Mixed` module is a bit different than the old module, so we would not even be able to easily put in CPP to conditionally depend on the right rope module depending on the `lsp` version. Finally, this means dropping support for GHC 9.0 since `lsp-2.4` does not support it.
Along the way I also fixed a minor issue related to showing type information returned by the LSP server, so that it uses `prettyTypeLine` to display the type on a single line (in my editor, when the type does not use a single line it gets cut off). For comparison see also #1610.
This refactoring was a big pain because a lot of things (names of types and constructors, locations of exports, etc...) changed from 1.x to 2.x, but there was not much in the way of documenting what had changed. =( I am pretty sure that all functionality has been preserved but I would appreciate independent confirmation.
This is also a prerequisite for updating other dependencies such as the `base` version (I will open a follow-up PR soon) since the old `lsp-1.x` versions do not allow many newer versions of various dependencies.
![Screenshot from 2024-02-04 22-25-24](https://github.com/swarm-game/swarm/assets/261693/6541523c-4279-4fdf-a8fe-c3c8af1aa169)
scripts/play.sh -i data/scenarios/Challenges/gallery.yaml --autoplay
## Code changes
In addition to the new scenario, this PR includes a code change to allow the order of items returned by the `tagmembers` to be specified by the scenario author.
Closes#1037.
The main changes are in `Swarm.Game.Scenario.Objective`.
- No longer export the `CompletionBuckets` type, nor the constructor or record accessors of the `ObjectiveCompletion` type
- Export folds such as `incompleteObjectives` (a `Fold` is like a read-only `Traversal`, i.e. it has multiple targets but does not allow setting/modification), as well as `allObjectives`.
- Export some custom functions such as `addCompleted` which properly maintains the internal invariants of `ObjectiveCompletion`.
The rest of the changes are simply adapting to use these new lenses + folds instead of directly accessing `CompletionBucket` values or using the underscored record field projections.
This simplifies the code in `Swarm.Game.Scenario.Objective` and hopefully makes the code safer (previously, it was possible in theory to violate `CompletionBuckets` invariants by directly modifying its fields).
Opens a live-reloading preview of the world in VS Code.
The renderer has been modified to optionally render a blank image instead of crashing upon invalid YAML.
## Prerequisites:
Install inotify tools:
sudo apt install inotify-tools
## Usage:
scripts/preview-world-vscode.sh data/scenarios/Fun/horton.yaml
Once the VS Code editor tabs are opened, one can press <kbd>CTRL</kbd> + <kbd>\\</kbd> (backslash) with the image selected to split the editor pane horizontally.
One may then navigate to the left-pane's copy of the image preview with <kbd>CTRL</kbd> + <kbd>PageUp</kbd>, and then <kbd>CTRL</kbd> + <kbd>w</kbd> will close the redundant image preview.
## Screenshot
![Screenshot from 2024-01-29 18-53-55](https://github.com/swarm-game/swarm/assets/261693/63a4728c-0ccb-4c08-8cde-61d65e8322b4)
In support of #1598 (for #1739).
Runs the `idle` benchmark for many more ticks and with many more robots to try to emphasize the effect of regressions and mitigate jitter.
Also factors out common code from the `benchmark-against-parent.sh` script.
Closes#1726
There are now two entry points to obtain a `GameplayAchievement`:
* `grantAchievementForRobot`
* `grantAchievement`
The latter enforces that the achievement is applicable to the current game mode (i.e. creative), while the former makes an additional check for validity of system robots.
This is an improvement over the status quo, but note that it is still currently possible to make a coding error in which an achievement specifies a requirement of **not** being a system robot, but the site at which the achievement is granted only calls `grantAchievement`, bypassing the robot validity check.
Closes#1715
Extract scene renderer to its own binary with `swarm-scenario` as its only dependency.
This was mainly enabled by moving the `Landscape` sub-record out of the `swarm-engine` sublibrary and into `swarm-scenario`, and refactoring rendering functions to only depend on `Landscape` rather than the entire `GameState`.
# Demo
stack build --fast swarm:swarm-scene && stack exec swarm-scene -- data/scenarios/Fun/horton.yaml --png -w 180 -h 150
![output](https://github.com/swarm-game/swarm/assets/261693/e2df3aad-3b65-46bf-8485-352facdffc76)
Builds upon #1587.
Extract all symbol names that are not native to the current project and insert them into our own custom spell checking dictionary's "words" list.
The premise is that symbols that *are* native to our project should be spellchecked, but foreign symbols that constitute unrecognized dictionary words are presumably intentionally spelled that way.
# Convention
Manually-added words (i.e. for names in code that we've written for this project) will go into `.vscode/settings.json`.
The automatically generated word list from third-party packages goes into `cspell.json`.
# Usage
scripts/spellcheck/autopopulate-spellchecker.sh
Measures the volume of an enclosed space.
A useful alternative to the `path` command for goal checking.
## Demo
scripts/play.sh -i data/scenarios/Testing/1747-volume-command.yaml --autoplay --speed 2
Closes#1741
Remaining blocker was to move logging to concrete robot.
Most notably, the `swarm-scenario` sublibrary has been purged of its temporal component, corroborated by the removal of the `time` package dependency.
The spellchecker in VS code can be quite noisy, given all of the esoteric terminology and shorthand variable names in code.
However, it is also super helpful for catching typos, if you can wade through the noise.
Adding a project-specific dictionary will significantly cut down on that noise.
Towards #1715 and #1043
Creates a new `swarm-scenario` sublibrary intended for scenario data that is independent of game state.
# Planned follow-ups
This PR is already pretty large, but there is still more that can be done regarding sublibrary reorganization/splitting.
* May want to pare-down a sublibrary exclusively for world-generation without all the other baggage of scenarios.
* `Swarm.Game.ScenarioInfo`, `Swarm.Game.Tick`, and `Swarm.Game.Scenario.Status` could probably be moved from `swarm-scenario` to `swarm-engine`.
Continuation of #1731.
Towards #1715 and #1043.
This refactoring step is a prerequisite for #1719 to extricate references to the `CESK` module from the base `RobotR` definition.
# In this PR:
* Extracts the `RobotContext` type to a new module
* Introduces a type family for the `RobotContext` field
# Motivation
Want to remove dependence of `TRobot` on the `RobotContext` record. However, a lens `trobotContext` had been added in #817 to fix#394.
# Test
scripts/run-tests.sh --test-arguments '--pattern "394-build-drill"'
Towards #1715 and #1043.
This refactoring step is a prerequisite for #1719 to extricate references to the `CESK` module from the base `RobotR` definition.
In this PR:
* `Swarm.Game.CESK` is imported qualified to more easily track usages
* A new `RobotMachine` type family is added to parameterize the `_machine` field.
* `CESK` is a new parameter to `addTRobot`
* `tea plant` (which now spawns rarely in a certain biome of the classic world) can be harvested to yield...
* `tea leaves`, which can be combined with `water` to produce...
* `cup of tea`, which, along with `atomic vector plotter` (= `rubber band` + `typewriter` + `compass` + `counter`), are ingredients for...
* `infinite improbability drive`, which enables `teleport`.
When used by a privileged robot (system robot or in creative mode), the `teleport` command continues to function as before. When used by an unprivileged robot, that is, via the `infinite improbability drive` device, a random entity spawns somewhere near the target location.
Closes#1041.
Fixes#1693. It's not necessarily appropriate to copy the entire `Display` record. So instead, we explicitly list the fields that should be inherited. For now, it is only `displayAttr`, but we could add `invisible` in the future (see #1670, #1663).
Tested to make sure behavior is preserved with:
```
scripts/play.sh -i scenarios/Challenges/Ranching/beekeeping.yaml --autoplay
```
Closes#1623.
Note that we still have to list both `vty-unix` and `vty-windows` in `stack.yaml` but that's OK; it just means they are both made available as extra dependencies, but only whichever is needed will actually be installed. In the codebase itself, we now get to avoid CPP on imports, and the code is quite a bit simpler.
Closes#845
**All changes are non-significant whitespace.** This can be verified with:
git show --ignore-all-space --ignore-blank-lines
in which the remaining changes are only elimination of manual word-wrap in descriptions.
Normalization is accomplished with this command:
scripts/normalize-all-scenarios.sh
This is an initial normalization pass that shall be a pre-requisite for #1713.
Towards #1043
Extracts `Swarm.Game.*` modules to their own sublibrary.
There was already pretty good separation along this boundary; just had to move three functions into a new module `Swarm.Util.Content`.