Skip to content

Commit

Permalink
Structure browser and recognizer (#1579)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
kostmo authored Nov 8, 2023
1 parent 990195f commit d63e7d8
Show file tree
Hide file tree
Showing 63 changed files with 2,559 additions and 139 deletions.
1 change: 1 addition & 0 deletions data/scenarios/Testing/00-ORDER.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ Achievements
1430-built-robot-ownership.yaml
1536-custom-unwalkable-entities.yaml
1535-ping
1575-structure-recognizer
11 changes: 11 additions & 0 deletions data/scenarios/Testing/1575-structure-recognizer/00-ORDER.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
1575-browse-structures.yaml
1575-nested-structure-definition.yaml
1575-construction-count.yaml
1575-handle-overlapping.yaml
1575-ensure-single-recognition.yaml
1575-ensure-disjoint.yaml
1575-overlapping-tiebreaker-by-largest.yaml
1575-overlapping-tiebreaker-by-location.yaml
1575-remove-structure.yaml
1575-swap-structure.yaml
1575-placement-occlusion.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
version: 1
name: Structure browser
description: |
Hit F6 to view the recognizable structures.
Only the subset of the structures marked with
"recognize: true" are browseable.
In particular, the "donut" structure is placed
in the map but not displayed in the F6 dialog.
creative: false
objectives:
- teaser: Build structure
goal:
- |
Build a "precious" structure
condition: |
foundStructure <- structure "precious" 0;
return $ case foundStructure (\_. false) (\_. true);
robots:
- name: base
dir: [1, 0]
devices:
- grabber
- treads
inventory:
- [50, flower]
- [50, log]
- [50, rock]
- [50, copper pipe]
- [50, iron gear]
- [50, quartz]
- [50, gold]
- [50, silver]
- [50, mithril]
- [50, cotton]
solution: |
move;
place "quartz";
move;
place "quartz";
move;
place "mithril";
structures:
- name: donut
structure:
palette:
'@': [dirt, rock]
mask: '.'
map: |
.@@@.
@@@@@
@@.@@
@@@@@
.@@@.
- name: diamond
recognize: true
description: "A diamond pattern of flowers"
structure:
mask: '.'
palette:
'x': [stone, flower]
map: |
...x...
..xxx..
.xxxxx.
xxxxxxx
.xxxxx.
..xxx..
...x...
- name: contraption
recognize: true
description: "A device for assembling useful widgets"
structure:
mask: '.'
palette:
'r': [stone, log]
'I': [stone, rock]
'l': [stone, copper pipe]
'g': [stone, iron gear]
map: |
rllllr
lIIIIl
lIIIgg
rlllgg
- name: precious
recognize: true
structure:
mask: '.'
palette:
'q': [stone, quartz]
'g': [stone, gold]
's': [stone, silver]
'm': [stone, mithril]
map: |
qgs
gsq
qqm
- name: smallish
recognize: true
structure:
mask: '.'
palette:
'q': [stone, quartz]
'm': [stone, mithril]
'c': [stone, cotton]
map: |
qqm
cqq
known: [flower, log, rock, copper pipe, iron plate]
world:
name: root
dsl: |
{blank}
palette:
'.': [grass]
'q': [grass, quartz]
'g': [grass, gold]
's': [grass, silver]
'm': [grass, mithril]
'c': [grass, cotton]
'B': [grass, null, base]
upperleft: [0, 0]
placements:
- src: donut
offset: [6, 0]
map: |
.qgs.........
.gsq.........
B............
.cqq.........
.............
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
version: 1
name: Structure recognizer - counting
description: |
Count the construction of several adjacent copies
creative: false
objectives:
- teaser: Build 12 structures
goal:
- |
Build 12 copies of the "green_jewel" structure
condition: |
foundGreen <- structure "green_jewel" 0;
return $ case foundGreen (\_. false) (\x. fst x >= 12);
robots:
- name: base
dir: [1, 0]
devices:
- ADT calculator
- branch predictor
- comparator
- dictionary
- grabber
- lambda
- logger
- strange loop
- treads
inventory:
- [108, pixel (G)]
solution: |
def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;
doN 6 (
doN 9 (place "pixel (G)"; move;);
doN 2 (turn right; move;);
doN 9 (place "pixel (G)"; move;);
doN 2 (turn left; move;);
);
structures:
- name: green_jewel
recognize: true
structure:
palette:
'g': [stone, pixel (G)]
map: |
ggg
ggg
ggg
known: [pixel (G)]
world:
name: root
dsl: |
{blank}
palette:
'.': [grass]
'B': [grass, null, base]
upperleft: [0, 0]
map: |
B........
.........
.........
.........
.........
.........
.........
.........
.........
.........
.........
.........
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
version: 1
name: Structure recognizer - Disjoint recognitions
description: |
Ensure that the completion of a second structure
template is not recognized if it overlaps
with a previously completed structure.
Player starts with 3 `silver`{=entity}. A win
should not be counted until all three are placed.
creative: false
objectives:
- teaser: Build 2 chessboards
prerequisite:
not: premature_win
goal:
- |
Build 2 of the same structure
condition: |
foundStructure <- structure "chessboard" 0;
return $ case foundStructure (\_. false) (\fs.
let boardCount = fst fs in
boardCount >= 2;
);
- id: premature_win
teaser: Don't count win early
optional: true
goal:
- |
Two structures shouldn't be recognized
while the bases still possesses `silver`{=entity}
condition: |
robotHasSilver <- as base {has "silver"};
foundStructure <- structure "chessboard" 0;
return $ case foundStructure (\_. false) (\fs.
let boardCount = fst fs in
boardCount >= 2 && robotHasSilver;
);
robots:
- name: base
dir: [0, -1]
devices:
- grabber
- treads
inventory:
- [3, silver]
solution: |
move;
turn left;
place "silver";
move; move;
place "silver";
move; move;
place "silver";
structures:
- name: chessboard
recognize: true
structure:
mask: '.'
palette:
'g': [stone, gold]
's': [stone, silver]
map: |
gsgs
sgsg
gsgs
sgsg
world:
name: root
dsl: |
{water}
palette:
'.': [grass, water]
'x': [grass, erase]
'g': [grass, gold]
's': [grass, silver]
'B': [grass, water, base]
upperleft: [0, 0]
map: |
...B....
gsgxgxgx
sgsgsgsg
gsgsgsgs
sgsgsgsg
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
version: 1
name: Structure recognizer - single recognition
description: |
Ensure that only a single structure is recognized
when placing an entity would complete more than one
structure template.
creative: false
objectives:
- teaser: Build 2 chessboards
prerequisite:
not: premature_win
goal:
- |
Build 2 of the same structure
condition: |
foundStructure <- structure "chessboard" 0;
return $ case foundStructure (\_. false) (\fs.
let boardCount = fst fs in
boardCount >= 2;
);
- id: premature_win
teaser: Don't count win early
optional: true
goal:
- |
Two structures shouldn't be recognized
while the bases still possesses `gold`{=entity}
condition: |
robotHasGold <- as base {has "gold"};
foundStructure <- structure "chessboard" 0;
return $ case foundStructure (\_. false) (\fs.
let boardCount = fst fs in
boardCount >= 2 && robotHasGold;
);
robots:
- name: base
dir: [0, -1]
devices:
- grabber
- treads
inventory:
- [1, gold]
- [1, silver]
solution: |
move;
place "silver";
move; move; move;
turn left;
move; move; move; move;
place "gold";
structures:
- name: chessboard
recognize: true
structure:
mask: '.'
palette:
'g': [stone, gold]
's': [stone, silver]
map: |
gsgs
sgsg
gsgs
sgsg
world:
name: root
dsl: |
{blank}
palette:
'.': [grass]
'g': [grass, gold]
's': [grass, silver]
'B': [grass, null, base]
upperleft: [0, 0]
map: |
...B....
gsg.gsgs
sgsgsgsg
gsgsgsgs
sgsgsgs.
Loading

0 comments on commit d63e7d8

Please sign in to comment.