Resource gathering + programming game
Go to file
Karl Ostmo 01ae0e45d7
react immediately to wakeups (#1736)
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.
2024-03-11 02:48:26 +00:00
.github autoformat cabal file (#1769) 2024-02-19 20:07:15 +00:00
.vscode extensible terrain (#1775) 2024-02-29 06:22:21 +00:00
app extensible terrain (#1775) 2024-02-29 06:22:21 +00:00
data react immediately to wakeups (#1736) 2024-03-11 02:48:26 +00:00
docs split scenario construction into separate sublibrary (#1719) 2024-01-24 21:11:14 +00:00
editors volume command (#1747) 2024-01-28 01:02:08 +00:00
example Key input handler (#1214) 2023-04-25 16:39:59 +00:00
images 0.4 release blog post (#1444) 2023-08-22 12:35:05 -05:00
scripts react immediately to wakeups (#1736) 2024-03-11 02:48:26 +00:00
src react immediately to wakeups (#1736) 2024-03-11 02:48:26 +00:00
test react immediately to wakeups (#1736) 2024-03-11 02:48:26 +00:00
web Render command matrix (#1658) 2024-01-26 01:02:14 +00:00
.gitignore tweak benchmarks (#1754) 2024-01-29 18:38:57 +00:00
.hlint.yaml more partial function bans (#1564) 2023-10-02 03:13:20 +00:00
.mergify.yml Update to depend on lsp-2.4.0.0 (#1762) 2024-02-12 11:05:39 -06:00
.restyled.yaml Fourmolu fixup (#1326) 2023-07-12 18:00:23 +00:00
.stan.toml exclude some stan observations 2021-09-15 06:39:21 -05:00
cabal.haskell-ci Update to depend on lsp-2.4.0.0 (#1762) 2024-02-12 11:05:39 -06:00
CHANGELOG.md 0.5 release (#1606) 2023-11-01 10:18:16 +00:00
CODE_OF_CONDUCT.md Adopt the Contributor Covenant Code of Conduct 2021-09-19 13:38:33 -05:00
COMMUNITY.md some updates to README and COMMUNITY 2022-10-05 17:52:52 -05:00
CONTRIBUTING.md update CONTRIBUTING guide, and remove info from README (#1512) 2023-09-23 21:46:04 +00:00
cspell.json autopopulate spellchecker (#1749) 2024-01-28 01:54:13 +00:00
DESIGN.md More minor edits (#693) 2022-09-18 12:04:33 +00:00
feedback.yaml Records (#1148) 2023-03-25 11:58:34 +00:00
fourmolu.yaml World description DSL (#1376) 2023-08-17 11:08:42 +00:00
hie.yaml.stack Add cabal to HLS configuration (#131) 2021-10-03 21:14:04 +00:00
LICENSE Update editors (#581) 2022-07-25 00:16:23 +02:00
NOTICE Use a new opaque type for robots instead of strings (#303) 2022-03-02 03:00:44 +00:00
pull_request_template.md Add a pull request template (#1434) 2023-08-20 19:43:27 +00:00
README.md update CONTRIBUTING guide, and remove info from README (#1512) 2023-09-23 21:46:04 +00:00
scenarios Documentation for scenario file format (#612) 2022-07-30 08:02:11 -04:00
stack.yaml Dependency updates (#1765) 2024-02-12 20:04:28 +00:00
swarm.cabal whitelist walkable entities (#1721) 2024-03-10 01:31:35 +00:00

Swarm

Build Status GitHub release (latest by date) Swarm release on Hackage Contributor Covenant ircchat GitHub Contributors

Swarm is a 2D programming and resource gathering game. Program your robots to explore the world and collect resources, which in turn allows you to build upgraded robots that can run more interesting and complex programs. More info can be found on the Swarm website.

World 0 after scanning a tree and making a log.

Contributing

See CONTRIBUTING.md for information about various ways you can contribute to Swarm development!

Building

If you just want to play the game, head over to the Swarm website for installation instructions. If you want to build Swarm from source (e.g. in order to contribute, or to test out the latest bleeding-edge unreleased features), read on.

  1. Clone the Swarm repository, e.g.

    git clone https://github.com/swarm-game/swarm.git
    
  2. If you don't already have the stack tool:

    1. Get the ghcup tool, a handy one-stop utility for managing all the different pieces of a Haskell toolchain.

    2. Use ghcup to install stack:

      ghcup install stack
      
  3. Now use stack to build and run Swarm:

    cd /path/to/the/swarm/repo
    stack run
    
  4. Go get a snack while stack downloads a Haskell compiler and all of Swarm's dependencies.

  5. You might also want to check out the scripts directory, which contains an assortment of useful scripts for developers.