beekeeping scenario (#1599)

Builds upon #1579

Requires player to constuct beehives to attract bees and make honey, which is then brewed as mead.

## Demo

    scripts/play.sh -i scenarios/Challenges/Ranching/beekeeping.yaml --autoplay

![image](https://github.com/swarm-game/swarm/assets/261693/f8c5c898-d865-4fe7-954d-c9e5b5f9a5c8)

Map:
![map](https://github.com/swarm-game/swarm/assets/261693/9c288edb-e71f-4a59-bd32-e1ecfdb1b60e)

## References

Mead hall inspiration: https://cartographyassets.com/assets/13507/the-mead-hall-of-the-clan-ulfgar-50-x-50/

![Mead-Hal-v1-No-Light-or-Shadow-with-grid-copy-scaled](https://github.com/swarm-game/swarm/assets/261693/a2d276f1-a522-499e-9a4b-7ce1df754107)
This commit is contained in:
Karl Ostmo 2023-11-27 19:39:28 -08:00 committed by GitHub
parent 44c2e607b3
commit e03251cc0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1123 additions and 1 deletions

View File

@ -638,7 +638,14 @@
attr: blue
char: 'B'
description:
- Locate and analyze structures placed in the world.
- This enables the `structure` and `floorplan` commands to
locate and analyze structures placed in the world.
- |
`structure : text -> int -> cmd (unit + (int * (int * int)))`
- Gets the x, y coordinates of the southwest corner of a constructed structure, by name and index.
- |
`floorplan : text -> cmd (int * int)`
- Gets the dimensions of a structure template.
properties: [portable]
capabilities: [structure]

View File

@ -1,3 +1,4 @@
beekeeping.yaml
capture.yaml
powerset.yaml
gated-paddock.yaml

View File

@ -0,0 +1,298 @@
// Spawns worker bees when structures are detected
def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;
def mod : int -> int -> int = \a. \b. a - (a/b)*b end;
def abs = \n. if (n < 0) {-n} {n} end;
def min = \x. \y. if (x < y) {x} {y} end;
def elif = \t. \then. \else. {if t then else} end
def else = \t. t end
def sumTuples = \t1. \t2.
(fst t1 + fst t2, snd t1 + snd t2);
end;
def mapTuple = \f. \t.
(f $ fst t, f $ snd t)
end;
def negateTuple = \t.
mapTuple (\x. -x) t;
end;
def subtractTuple = \t1. \t2.
sumTuples t1 $ negateTuple t2;
end;
// Deprecated
def moveTuple = \tup.
let x = fst tup in
let y = snd tup in
turn $ if (x > 0) {east} {west};
doN (abs x) move;
turn $ if (y > 0) {north} {south};
doN (abs y) move;
end;
def randomDir =
r <- random 4;
return $ if (r == 1) {north}
$ elif (r == 2) {west}
$ elif (r == 3) {south}
$ else {east};
end;
def moveHorizontal = \maxDirect. \dist.
turn $ if (dist > 0) {east} {west};
doN (min maxDirect $ abs dist) move;
end;
def moveVertical = \maxDirect. \dist.
turn $ if (dist > 0) {north} {south};
doN (min maxDirect $ abs dist) move;
end;
def randomStep =
randDir <- randomDir;
turn randDir;
move;
end;
def moveToward = \maxDirect. \goal.
currLocOrig <- whereami;
if (currLocOrig == goal) {} {
// Include some random motion
randomStep;
currLoc <- whereami;
let delta = subtractTuple goal currLoc in
let x = fst delta in
let y = snd delta in
moveHorizontal maxDirect x;
moveVertical maxDirect y;
moveToward maxDirect goal;
}
end;
def watchForHoneycombRemoval = \dist.
if (dist > 0) {
move;
honeycombHere <- ishere "honeycomb";
if honeycombHere {
watch down;
} {};
watchForHoneycombRemoval $ dist - 1;
} {};
end;
/**
Tries to find an open cell to deposit
the honeycomb. Gives up when distance
threshold exceeded.
*/
def depositHoneycomb = \dist.
if (dist < 5) {
emptyHere <- isempty;
if emptyHere {
place "honeycomb";
} {
move;
depositHoneycomb $ dist + 1;
};
} {
turn back;
watchForHoneycombRemoval dist;
// Hibernate
wait 2000;
// Alternative method to get rid of honeycomb
make "buzz";
};
end;
def goToHive = \hiveLoc.
let depositLoc = (fst hiveLoc - 1, snd hiveLoc) in
moveToward 2 depositLoc;
turn north;
depositHoneycomb 0;
end;
/**
Harvests an item when reached
*/
def takeStepTowardItem = \item.
// NOTE: Max radius is hard-coded to 256
// (see maxSniffRange in Syntax.hs)
direction <- chirp item;
if (direction == down) {
// Need a try block in case
// another bee gets here first
try {
harvest;
return ();
} {};
} {
// Include some random motion
r <- random 4;
if (r == 0) {
randomStep;
} {
turn direction;
move;
};
takeStepTowardItem item;
}
end;
/**
Searches through the existing instances of
a given structure template, starting at a supplied
index.
Either returns the (potentially new) index of the structure
(in the case that more had been built since the last check),
or unit. Re-using the newly found index amortizes the "search"
within the structure list over many ticks to constant time
rather than linear time.
*/
def findStructureNewIndex = \remainingCount. \structureLoc. \lastIdx.
if (remainingCount > 0) {
foundStructure <- structure "beehive" lastIdx;
case foundStructure (\_. return $ inL ()) (\fs.
if (structureLoc == snd fs) {
return $ inR lastIdx;
} {
findStructureNewIndex (remainingCount - 1) structureLoc $ lastIdx + 1;
}
);
} {
return $ inL ();
}
end;
def workerProgram = \hiveIdx. \structureLoc.
eitherFoundStructure <- structure "beehive" hiveIdx;
case eitherFoundStructure return (\fs.
let hasSameStructure = structureLoc == snd fs in
if hasSameStructure {
try {make "honeycomb";} {};
hasHoneycomb <- has "honeycomb";
if hasHoneycomb {
goToHive structureLoc;
} {
takeStepTowardItem "wildflower";
return ();
};
workerProgram hiveIdx structureLoc;
} {
eitherNewIdx <- findStructureNewIndex (fst fs) structureLoc hiveIdx;
case eitherNewIdx
(\_. selfdestruct)
(\newIdx. workerProgram newIdx structureLoc);
}
);
end;
def mkBeeName = \structureLoc.
"bee" ++ format structureLoc;
end;
def workerProgramInit = \beename. \hiveIdx. \structureLoc.
setname beename;
appear "B";
workerProgram hiveIdx structureLoc;
end;
def createWorkerForStructure = \structureIdx. \fs.
// Build worker bee, assign ID, location
create "wax gland";
create "proboscis";
create "ADT calculator";
create "beaglepuss";
create "bitcoin";
create "branch predictor";
create "comparator";
create "compass";
create "detonator";
create "dictionary";
create "fast grabber";
create "GPS receiver";
create "harvester";
create "hourglass";
create "lambda";
create "net";
create "rolex";
create "scanner";
create "strange loop";
create "solar panel";
create "treads";
create "workbench";
teleport self $ snd fs;
let beename = mkBeeName (snd fs) in
build {
require 1 "wax gland";
workerProgramInit beename structureIdx $ snd fs;
};
return ();
end;
def associateAllHives = \remainingCount. \idx.
if (remainingCount > 0) {
foundStructure <- structure "beehive" idx;
case foundStructure return (\fs.
let beename = mkBeeName (snd fs) in
try {
// Fails if the robot does not exist
robotnamed beename;
return ();
} {
createWorkerForStructure idx fs;
// Give the child robot time to register its new
// name so that we don't end up spawning multiple
// bees for the same location
wait 1;
};
associateAllHives (remainingCount - 1) (idx + 1);
);
} {}
end;
/**
Each tick, iterates through all hives,
and makes sure a "bee" robot is associated with
their location.
If a structure exists without such an association,
creates a bee named after the location.
*/
def observeHives =
// This invocation is just to get the total structure count.
// We will invoke it again once per iteration of 'associateAllHives'.
foundStructure <- structure "beehive" 0;
case foundStructure return (\fs.
associateAllHives (fst fs) 0;
);
// Wait at least 1 tick so that we do not spin infinitely until
// we saturate our computation quota for the tick.
wait 1;
observeHives;
end;
def go =
instant $ observeHives;
end;
go;

View File

@ -0,0 +1,362 @@
def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;
def intersperse = \n. \f2. \f1. if (n > 0) {
f1;
if (n > 1) {
f2;
} {};
intersperse (n - 1) f2 f1;
} {};
end;
def abs = \n. if (n < 0) {-n} {n} end;
def sumTuples = \t1. \t2.
(fst t1 + fst t2, snd t1 + snd t2);
end;
def mapTuple = \f. \t.
(f $ fst t, f $ snd t)
end;
def negateTuple = \t.
mapTuple (\x. -x) t;
end;
def subtractTuple = \t1. \t2.
sumTuples t1 $ negateTuple t2;
end;
def moveTuple = \tup.
let x = fst tup in
let y = snd tup in
turn $ if (x > 0) {east} {west};
doN (abs x) move;
turn $ if (y > 0) {north} {south};
doN (abs y) move;
end;
def nextRow = \d.
intersperse 2 move $ turn d;
end;
def harvestTrees =
turn back;
doN 14 move;
turn left;
intersperse 4 (nextRow left;) (
intersperse 15 move harvest;
nextRow right;
intersperse 15 move harvest;
);
end;
def slatRow =
let item = "honey frame" in
intersperse 3 move $ place item;
end;
def buildHive =
// Make 16 boards
doN 4 (make "log"; make "board");
// Make 9 honey frames
doN 3 $ make "log";
doN 3 (make "board"; make "honey frame");
doN 4 (intersperse 4 move (place "board"); turn right; move;);
turn right;
move;
slatRow;
nextRow left;
slatRow;
nextRow right;
slatRow;
end;
def moveToNextHive =
doN 7 move;
end;
/* Makes 8 staves */
def makeStaveBatch =
// 1 per tree
make "log";
// 4 per log
make "board";
// 2 per board
doN 4 $ make "stave";
end;
def buildCasks = \caskCount.
// 40 staves
doN 5 makeStaveBatch;
doN 4 $ make "steel hoop";
doN caskCount $ make "cask";
end;
/*
Moves forward until finding objective
item, stops when a gap is reached.
*/
def collectContiguous = \maxdist. \item. \hadFound. \dist.
if (dist <= maxdist) {
honeycombHere <- ishere item;
if honeycombHere {
grab;
move;
collectContiguous maxdist item true $ dist + 1;
} {
if hadFound {
return dist;
} {
move;
collectContiguous maxdist item false $ dist + 1;
}
}
} {
return dist;
}
end;
def collectHoneycomb =
distTravelled <- collectContiguous 10 "honeycomb" false 0;
turn back;
doN distTravelled move;
end;
def collectAllHoneycombs = \targetCount.
honeycombHere <- ishere "honeycomb";
if honeycombHere {} {
watch down;
wait 2000;
};
intersperse 4 (turn left; doN 9 move; turn left;) collectHoneycomb;
currentCount <- count "honeycomb";
if (currentCount < targetCount) {
turn right;
doN 27 move;
turn right;
collectAllHoneycombs targetCount;
} {
return currentCount;
};
end;
def moveUntilBlocked =
thing <- scan forward;
let isBlocked = case thing (\_. false) (\x. x == "lakewater") in
if isBlocked {} {
move;
moveUntilBlocked;
}
end;
def getLakewater = \caskCount.
turn right;
doN 2 move;
turn left;
doN 5 move;
turn right;
moveUntilBlocked;
doN caskCount $ use "siphon" forward;
end;
def pickRock =
isRock <- ishere "rock";
if isRock {
grab;
return ();
} {};
end;
def collectRocks =
doN 22 move;
turn right;
doN 59 move;
turn left;
doN 7 (
intersperse 15 move pickRock;
nextRow right;
intersperse 15 move pickRock;
nextRow left;
);
end;
def makeTables =
doN 18 (make "log"; make "board";);
doN 36 $ make "table";
turn right;
move;
turn right;
doN 2 move;
doN 9 (swap "table"; move);
doN 8 move;
doN 9 (swap "table"; move);
turn left;
doN 2 move;
turn left;
move;
doN 9 (swap "table"; move);
doN 8 move;
doN 9 (swap "table"; move);
end;
def buildTavern =
// x16 per rock =
doN 12 $ make "stone tile";
doN 2 (
intersperse 30 move $ place "stone tile";
nextRow left;
intersperse 30 move $ place "stone tile";
nextRow right;
);
intersperse 30 move $ place "stone tile";
makeTables;
// hearth
nextRow right;
doN 12 move;
intersperse 4 move (make "hearth"; swap "hearth");
turn right;
doN 6 move;
turn right;
intersperse 4 move (make "archway"; place "archway");
nextRow right;
intersperse 4 move (make "archway"; place "archway");
move;
// Make enough logs for 70 wall pieces
doN 35 (make "log"; make "wall");
intersperse 14 move (place "wall");
turn left;
doN 6 (move; place "wall");
turn left;
doN 31 (move; place "wall";);
turn left;
doN 6 (move; place "wall");
turn left;
doN 13 (move; place "wall");
end;
def combCollectionLoop = \targetCount.
currentCount <- count "honeycomb";
if (currentCount < targetCount) {
watch down;
wait 2000;
collectHoneycomb;
turn back;
combCollectionLoop targetCount;
} {}
end;
def buildRobot = \targetCount. \meetingLoc.
// Unpack the "botkit"
make "solar panel";
build {
require 8 "tree";
buildHive;
// Move to northwest corner
turn back;
doN 3 move;
turn left;
doN 4 move;
turn left;
combCollectionLoop targetCount;
currLoc <- whereami;
let delta = subtractTuple meetingLoc currLoc in
moveTuple delta;
// Assume that the base has already arrived
// at the rendezvous point
honeycombCount <- count "honeycomb";
doN honeycombCount $ give parent "honeycomb";
};
end;
def placeHives = \targetCount. \meetingLoc.
buildRobot targetCount meetingLoc;
doN 38 move;
buildRobot targetCount meetingLoc;
doN 56 move;
turn right;
doN 15 move;
turn left;
buildRobot targetCount meetingLoc;
doN 2 move;
turn right;
doN 26 move;
turn left;
buildRobot targetCount meetingLoc;
end;
def acceptHoneyDeliveries =
currentCount <- count "honeycomb";
if (currentCount < 60) {
wait 32;
acceptHoneyDeliveries;
} {
// Use the "honey extractor"
doN currentCount $ make "honey";
}
end;
def go =
harvestTrees;
turn east;
doN 19 move;
let meetingLoc = (0, -12) in
placeHives 15 meetingLoc;
turn right;
buildCasks 2;
getLakewater 2;
turn right;
collectRocks;
doN 7 move;
turn left;
doN 80 move;
buildTavern;
nextRow right;
currLoc <- whereami;
let delta = subtractTuple meetingLoc currLoc in
moveTuple delta;
acceptHoneyDeliveries;
doN 4 salvage;
doN 2 $ make "mead";
end;
go;

View File

@ -0,0 +1,453 @@
version: 1
name: Beekeeping
author: Karl Ostmo
description: |
Bootstrap an organic *bee*verage industry.
creative: false
seed: 2
attrs:
- name: bee
fg: '#ffff00'
bg: '#000000'
- name: water_cask
fg: '#4488ff'
bg: '#8B4513'
- name: mybase
fg: '#e0c0e0'
- name: iceblue
fg: '#ddddff'
objectives:
- teaser: Apiarist
goal:
- |
Build a `beehive`{=structure}.
- |
This will attract bees, which will gather
`nectar`{=entity} from nearby `wildflower`{=entity}s.
- |
Do not crowd hives; provide at least one cell margin
between them.
- |
Given that `wildflower`{=entity}s cannot be relocated,
judicious placement is essential for efficient
`honeycomb`{=entity} production.
condition: |
foundStructure <- structure "beehive" 0;
return $ case foundStructure (\_. false) (\_. true);
- teaser: Collect honeycomb
goal:
- |
Collect `honeycomb`{=entity} from the `beehive`{=structure}s.
- |
After gathering a certain amount of nectar, a bee will return
to its hive and place honeycomb alongside it.
A finite amount of honeycomb may be accumulated before the
bee becomes dormant.
condition: |
as base {
has "honeycomb"
}
- teaser: Cooper
goal:
- |
Make a `cask`{=entity}.
- |
Use local timber and your on-hand supply of iron material.
- |
You may `use "siphon" forward` when positioned in front of
a lake to fill a `cask`{=entity} with `lakewater`{=entity}.
condition: |
as base {
has "cask"
}
- teaser: Brewmeister
goal:
- |
Ferment 2 barrels of `mead`{=entity}.
condition: |
as base {
meadCount <- count "mead";
return $ meadCount >= 2;
}
- teaser: Tavern keeper
optional: true
goal:
- |
Construct a `mead hall`{=structure}.
condition: |
foundStructure <- structure "mead hall" 0;
return $ case foundStructure (\_. false) (\_. true);
robots:
- name: base
display:
attr: mybase
dir: [1, 0]
devices:
- 3D printer
- blueprint
- branch predictor
- ADT calculator
- clock
- comparator
- compass
- counter
- dictionary
- fast grabber
- GPS receiver
- harvester
- hearing aid
- honey extractor
- keyboard
- lambda
- logger
- net
- rolex
- scanner
- siphon
- strange loop
- toolkit
- treads
- welder
- workbench
inventory:
- [20, iron plate]
- [4, botkit]
- name: queenbee
dir: [1, 0]
system: true
display:
invisible: true
char: 'Q'
attr: bee
program:
run "scenarios/Challenges/Ranching/_beekeeping/queenbee.sw"
solution: |
run "scenarios/Challenges/Ranching/_beekeeping/solution.sw"
structures:
- name: beehive
recognize: true
structure:
palette:
'-': [dirt, honey frame]
'b': [dirt, board]
map: |
bbbbb
b---b
b---b
b---b
bbbbb
- name: mead hall
recognize: true
structure:
palette:
'w': [dirt, wall]
't': [dirt, table]
'h': [dirt, hearth]
'a': [dirt, archway]
'.': [stone, stone tile]
mask: 'x'
map: |
wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww
w.............hhhh.............w
w..ttttttttt........ttttttttt..w
w..............................w
w..ttttttttt........ttttttttt..w
w..............................w
wwwwwwwwwwwwwwaaaawwwwwwwwwwwwww
xxxxxxxxxxxxxxaaaaxxxxxxxxxxxxxx
entities:
- name: wildflower
display:
attr: flower
char: '*'
description:
- A delicate flower that grows wild in local meadows.
Produces `nectar`{=entity} when `harvest`ed.
properties: [known, growable]
yields: nectar
growth: [80, 120]
- name: botkit
display:
attr: device
char: 'k'
description:
- All the essentials to equip your own "worker bee"
properties: [known, portable]
- name: reed
display:
attr: plant
char: 'r'
description:
- Reeds, grow near water
properties: [known, portable, growable]
- name: honeycomb
display:
char: 'x'
attr: gold
description:
- Product of bees that have consumed nectar
properties: [known, portable]
- name: proboscis
display:
char: 'p'
attr: device
description:
- Senses direction to nectar-producing flowers
properties: [known, portable]
capabilities: [detectdirection, structure]
- name: honey
display:
char: 'h'
attr: gold
description:
- Pure liquid honey
properties: [known, portable]
- name: mead
display:
char: 'm'
description:
- Honey-based alcoholic beverage
properties: [known, portable]
- name: honey extractor
display:
char: 'e'
attr: device
description:
- Device for extracting honey from the comb
properties: [known, portable]
- name: buzz
display:
char: 'z'
description:
- Result of discarding surplus honeycomb
properties: [known, portable]
- name: wax gland
display:
char: 'g'
description:
- Required to make honeycomb
properties: [known]
- name: nectar
display:
char: 'n'
attr: gold
description:
- Obtained from wildflowers
properties: [known, portable]
- name: honey frame
display:
char: '-'
attr: iceblue
description:
- Internal component of a beehive
properties: [known, portable]
- name: stave
display:
char: 'l'
attr: wood
description:
- Wooden plank comprising the sides of a cask
properties: [known, portable]
- name: cask
display:
char: 'c'
attr: wood
description:
- Wooden barrel for liquids
properties: [known, portable]
- name: water cask
display:
char: 'c'
attr: water_cask
description:
- Water-filled cask
properties: [known, portable]
- name: lakewater
display:
attr: water
char: ' '
description:
- Potable water from a lake
properties: [known, infinite, liquid]
- name: siphon
display:
char: 's'
attr: device
description:
- Used to fill a cask with water
properties: [known, portable]
- name: steel hoop
display:
char: 'o'
attr: iron
description:
- Binds staves into a cask
properties: [known, portable]
- name: wall
display:
char: 'w'
attr: wood
description:
- Outer walls of the building
properties: [known, unwalkable]
- name: table
display:
char: 't'
attr: wood
description:
- A segment of banquet table
properties: [known, portable]
- name: hearth
display:
char: 'h'
attr: rock
description:
- Encloses a fire to warm the hall
properties: [known, unwalkable]
- name: archway
display:
char: 'a'
attr: rock
description:
- Grand entrance
properties: [known, portable]
- name: stone tile
display:
char: '.'
attr: rock
description:
- Refined flooring
properties: [known, portable]
recipes:
- in:
- [1, botkit]
out:
- [1, branch predictor]
- [1, ADT calculator]
- [1, blueprint]
- [1, clock]
- [1, comparator]
- [1, compass]
- [1, counter]
- [1, dictionary]
- [1, grabber]
- [1, GPS receiver]
- [1, harvester]
- [1, lambda]
- [1, logger]
- [1, net]
- [1, rolex]
- [1, scanner]
- [1, solar panel]
- [1, strange loop]
- [1, treads]
- [1, welder]
- [1, workbench]
- in:
- [1, board]
out:
- [3, honey frame]
- in:
- [1, rock]
out:
- [16, stone tile]
- in:
- [2, rock]
out:
- [1, hearth]
- in:
- [4, rock]
out:
- [1, archway]
- in:
- [2, board]
out:
- [1, table]
- in:
- [1, rock]
- [1, log]
out:
- [2, wall]
- in:
- [1, board]
out:
- [2, stave]
- in:
- [2, iron plate]
out:
- [1, steel hoop]
- in:
- [20, stave]
- [2, steel hoop]
out:
- [1, cask]
- in:
- [1, cask]
- [1, lakewater]
out:
- [1, water cask]
- [1, lakewater]
required:
- [1, siphon]
- in:
- [16, nectar]
out:
- [1, honeycomb]
required:
- [1, wax gland]
- in:
- [1, water cask]
- [30, honey]
out:
- [1, mead]
- in:
- [1, honeycomb]
out:
- [1, honey]
required:
- [1, honey extractor]
- in:
- [1, honeycomb]
out:
- [1, buzz]
required:
- [1, wax gland]
known: [tree, rock, board]
world:
dsl: |
let
flowerNoise = perlin seed 1 0.15 0.0,
stoneNoise = perlin (seed + 1) 1 0.05 0.0,
lakeNoise = perlin seed 1 0.02 0.0,
forestNoise = perlin seed 2 0.04 1.0,
flowers = flowerNoise > 0.65,
rubble = stoneNoise > 0.8,
rock = stoneNoise > 0.9,
outerShore = -0.55 <= lakeNoise && lakeNoise < -0.5,
innerShore = lakeNoise < -0.55,
lakes = lakeNoise < -0.6,
trees = forestNoise > 0.8
in
overlay
[ {grass}
, mask (flowers && (x + y) % 3 == 0) {wildflower}
, mask (rubble && (x + y) % 2 == 0) {rock}
, mask rock {rock}
, mask trees {tree}
, mask (outerShore && (x + y) % 2 == 0) {reed}
, mask innerShore {reed}
, mask lakes {lakewater}
]
upperleft: [0, 0]
offset: false
palette:
'B': [grass, erase, base]
'Q': [grass, erase, queenbee]
'.': [grass, erase]
map: |
Q....
.....
..B..
.....
.....

View File

@ -241,6 +241,7 @@ testScenarioSolutions rs ui =
, testGroup
"Ranching"
[ testSolution Default "Challenges/Ranching/capture"
, testSolution (Sec 60) "Challenges/Ranching/beekeeping"
, testSolution (Sec 10) "Challenges/Ranching/powerset"
, testSolution (Sec 30) "Challenges/Ranching/gated-paddock"
]