mirror of
https://github.com/swarm-game/swarm.git
synced 2024-09-11 14:46:33 +03:00
Pathfinding command (#1523)
Closes #836 # Tests scripts/run-tests.sh --test-arguments '--pattern "Pathfinding"' # Demo scripts/play.sh -i data/scenarios/Testing/836-pathfinding/836-automatic-waypoint-navigation.yaml --autoplay
This commit is contained in:
parent
ab7e5b6d8b
commit
e06e04f622
@ -76,7 +76,7 @@ circlerProgram =
|
||||
|
||||
-- | Initializes a robot with program prog at location loc facing north.
|
||||
initRobot :: ProcessedTerm -> Location -> TRobot
|
||||
initRobot prog loc = mkRobot () Nothing "" mempty (Just $ Cosmic DefaultRootSubworld loc) north defaultRobotDisplay (initMachine prog Context.empty emptyStore) [] [] False False 0
|
||||
initRobot prog loc = mkRobot () Nothing "" mempty (Just $ Cosmic DefaultRootSubworld loc) north defaultRobotDisplay (initMachine prog Context.empty emptyStore) [] [] False False mempty 0
|
||||
|
||||
-- | Creates a GameState with numRobot copies of robot on a blank map, aligned
|
||||
-- in a row starting at (0,0) and spreading east.
|
||||
|
@ -1,131 +1,11 @@
|
||||
// Algorithm:
|
||||
// ----------
|
||||
// Maintain current direction until a wall is encountered.
|
||||
// Then enter "wall-following mode".
|
||||
// This mode presumes the wall is not a loop.
|
||||
// Wall-following mode exploits recursion to keep track of how many left turns were made
|
||||
// and then unwinds them again by ensuring each is paired with a right turn.
|
||||
// Once the recursion is fully unwound, the robot proceeds along its original direction
|
||||
// (though it may now be laterally displaced).
|
||||
//
|
||||
// (If it was a loop, then an "oriented breadcrumb" would need to be left.
|
||||
// The breadcrumb is oriented in case a single-width passage is backtracked
|
||||
// along the opposite wall.)
|
||||
|
||||
/** A "gate" is walkable, so we need to supplement the "blocked" check with this function.
|
||||
Since fences are "unwalkable", they do not need to be mentioned in this function.
|
||||
*/
|
||||
def isFenced =
|
||||
s <- scan forward;
|
||||
return (
|
||||
case s
|
||||
(\_. false)
|
||||
(\x. x == "gate")
|
||||
);
|
||||
end;
|
||||
|
||||
def isBlockedOrFenced =
|
||||
b <- blocked;
|
||||
f <- isFenced;
|
||||
return (b || f);
|
||||
end;
|
||||
|
||||
// Returns true if we've already placed two
|
||||
// breadcrumbs on a given tile, false otherwise.
|
||||
def leaveBreadcrumbs =
|
||||
|
||||
let bc1 = "fresh breadcrumb" in
|
||||
let bc2 = "treaded breadcrumb" in
|
||||
|
||||
wasTraversedOnce <- ishere bc1;
|
||||
if wasTraversedOnce {
|
||||
_crumb <- grab;
|
||||
make bc2;
|
||||
place bc2;
|
||||
return false;
|
||||
} {
|
||||
wasTraversedTwice <- ishere bc2;
|
||||
if wasTraversedTwice {
|
||||
return true;
|
||||
} {
|
||||
// Make sure nothing's in the way before we place
|
||||
// our breadcrumb:
|
||||
x <- scan down;
|
||||
case x return (\y.
|
||||
// If we're on a water tile, get rid of
|
||||
// it with our special "drilling" recipe
|
||||
if (y == "water") {
|
||||
drill down;
|
||||
// Nothing will remain on the ground.
|
||||
// after making the "steam" via
|
||||
// the drilling recipe.
|
||||
return ();
|
||||
} {
|
||||
grab;
|
||||
return ();
|
||||
};
|
||||
);
|
||||
|
||||
make bc1;
|
||||
place bc1;
|
||||
return false;
|
||||
};
|
||||
};
|
||||
end;
|
||||
|
||||
def goForwardToPatrol = \wasBlocked.
|
||||
b <- isBlockedOrFenced;
|
||||
if b {
|
||||
turn left;
|
||||
goForwardToPatrol true;
|
||||
turn right;
|
||||
goForwardToPatrol false;
|
||||
} {
|
||||
if wasBlocked {
|
||||
isLoop <- leaveBreadcrumbs;
|
||||
if isLoop {
|
||||
fail "loop";
|
||||
} {};
|
||||
} {};
|
||||
move;
|
||||
};
|
||||
end;
|
||||
|
||||
/**
|
||||
There should only be one place in the
|
||||
code where an exception is thrown: that is,
|
||||
if a treaded breadcrumb is encountered.
|
||||
*/
|
||||
def checkIsEnclosedInner =
|
||||
try {
|
||||
goForwardToPatrol false;
|
||||
// Water is the outer boundary
|
||||
hasWater <- ishere "water";
|
||||
if hasWater {
|
||||
return false;
|
||||
} {
|
||||
checkIsEnclosedInner;
|
||||
};
|
||||
} {
|
||||
return true;
|
||||
};
|
||||
return b;
|
||||
end;
|
||||
|
||||
def checkIsEnclosed =
|
||||
|
||||
// The "evaporator" drill is used
|
||||
// to clear water tiles.
|
||||
let specialDrill = "evaporator" in
|
||||
create specialDrill;
|
||||
equip specialDrill;
|
||||
|
||||
// NOTE: System robots can walk on water
|
||||
// so we only need this if we want to
|
||||
// demo the algorithm with a player robot.
|
||||
// create "boat";
|
||||
// equip "boat";
|
||||
|
||||
checkIsEnclosedInner;
|
||||
maybePath <- path (inL ()) (inR "water");
|
||||
case maybePath (\_. return True) (\_. return False);
|
||||
end;
|
||||
|
||||
def boolToInt = \b. if (b) {return 1} {return 0}; end;
|
||||
@ -215,35 +95,13 @@ def getValForSheepIndex = \predicateCmd. \i.
|
||||
}
|
||||
end;
|
||||
|
||||
/**
|
||||
There are 3 sheep.
|
||||
They have indices 1, 2, 3.
|
||||
(The base has index 0).
|
||||
|
||||
THIS DOES NOT WORK!
|
||||
*/
|
||||
def countSheepWithRecursive = \predicateCmd. \i.
|
||||
|
||||
if (i > 0) {
|
||||
val <- getValForSheepIndex predicateCmd i;
|
||||
recursiveCount <- countSheepWithRecursive predicateCmd $ i - 1;
|
||||
return $ val + recursiveCount;
|
||||
} {
|
||||
return 0;
|
||||
}
|
||||
end;
|
||||
|
||||
|
||||
def countSheepWith = \predicateCmd.
|
||||
|
||||
val1 <- getValForSheepIndex predicateCmd 1;
|
||||
val2 <- getValForSheepIndex predicateCmd 2;
|
||||
val3 <- getValForSheepIndex predicateCmd 3;
|
||||
return $ val1 + val2 + val3;
|
||||
|
||||
end;
|
||||
|
||||
|
||||
justFilledGap <- as base {
|
||||
isStandingOnBridge;
|
||||
};
|
||||
|
@ -1,26 +1,7 @@
|
||||
// A "sheep" that wanders around randomly.
|
||||
|
||||
/** A "gate" is walkable, so we need to supplement the "blocked" check with this function.
|
||||
Since fences are "unwalkable", they do not need to be mentioned in this function.
|
||||
*/
|
||||
def isFenced =
|
||||
s <- scan forward;
|
||||
return (
|
||||
case s
|
||||
(\_. false)
|
||||
(\x. x == "gate")
|
||||
);
|
||||
end;
|
||||
|
||||
def isBlockedOrFenced =
|
||||
b <- blocked;
|
||||
f <- isFenced;
|
||||
return (b || f);
|
||||
end;
|
||||
|
||||
def elif = \p.\t.\e. {if p t e} end;
|
||||
|
||||
|
||||
def turnToClover = \direction.
|
||||
|
||||
x <- scan direction;
|
||||
@ -95,7 +76,7 @@ forever (
|
||||
dist <- random 3;
|
||||
repeat dist (
|
||||
|
||||
b <- isBlockedOrFenced;
|
||||
b <- blocked;
|
||||
if b {} {
|
||||
move;
|
||||
};
|
||||
|
@ -7,4 +7,5 @@ SCENARIO_FILE=$PARENT_DIR/gated-paddock.yaml
|
||||
|
||||
PROGRAM=$(cat $SCRIPT_DIR/enclosure-checking.sw | sed -e 's/[[:blank:]]\+$//') yq -i '.objectives[0].condition = strenv(PROGRAM) | .objectives[].condition style="literal"' $SCENARIO_FILE
|
||||
|
||||
stack run -- --scenario $SCENARIO_FILE --run $SCRIPT_DIR/fence-construction.sw --cheat
|
||||
stack build --fast
|
||||
stack exec swarm -- --scenario $SCENARIO_FILE --run $SCRIPT_DIR/fence-construction.sw --cheat
|
@ -21,134 +21,14 @@ objectives:
|
||||
Note that you can use the `drill` command (by way of the `post puller`{=entity} tool)
|
||||
to demolish a `fence`{=entity} that has been `place`d.
|
||||
condition: |-
|
||||
// Algorithm:
|
||||
// ----------
|
||||
// Maintain current direction until a wall is encountered.
|
||||
// Then enter "wall-following mode".
|
||||
// This mode presumes the wall is not a loop.
|
||||
// Wall-following mode exploits recursion to keep track of how many left turns were made
|
||||
// and then unwinds them again by ensuring each is paired with a right turn.
|
||||
// Once the recursion is fully unwound, the robot proceeds along its original direction
|
||||
// (though it may now be laterally displaced).
|
||||
//
|
||||
// (If it was a loop, then an "oriented breadcrumb" would need to be left.
|
||||
// The breadcrumb is oriented in case a single-width passage is backtracked
|
||||
// along the opposite wall.)
|
||||
|
||||
/** A "gate" is walkable, so we need to supplement the "blocked" check with this function.
|
||||
Since fences are "unwalkable", they do not need to be mentioned in this function.
|
||||
*/
|
||||
def isFenced =
|
||||
s <- scan forward;
|
||||
return (
|
||||
case s
|
||||
(\_. false)
|
||||
(\x. x == "gate")
|
||||
);
|
||||
end;
|
||||
|
||||
def isBlockedOrFenced =
|
||||
b <- blocked;
|
||||
f <- isFenced;
|
||||
return (b || f);
|
||||
end;
|
||||
|
||||
// Returns true if we've already placed two
|
||||
// breadcrumbs on a given tile, false otherwise.
|
||||
def leaveBreadcrumbs =
|
||||
|
||||
let bc1 = "fresh breadcrumb" in
|
||||
let bc2 = "treaded breadcrumb" in
|
||||
|
||||
wasTraversedOnce <- ishere bc1;
|
||||
if wasTraversedOnce {
|
||||
_crumb <- grab;
|
||||
make bc2;
|
||||
place bc2;
|
||||
return false;
|
||||
} {
|
||||
wasTraversedTwice <- ishere bc2;
|
||||
if wasTraversedTwice {
|
||||
return true;
|
||||
} {
|
||||
// Make sure nothing's in the way before we place
|
||||
// our breadcrumb:
|
||||
x <- scan down;
|
||||
case x return (\y.
|
||||
// If we're on a water tile, get rid of
|
||||
// it with our special "drilling" recipe
|
||||
if (y == "water") {
|
||||
drill down;
|
||||
// Nothing will remain on the ground.
|
||||
// after making the "steam" via
|
||||
// the drilling recipe.
|
||||
return ();
|
||||
} {
|
||||
grab;
|
||||
return ();
|
||||
};
|
||||
);
|
||||
|
||||
make bc1;
|
||||
place bc1;
|
||||
return false;
|
||||
};
|
||||
};
|
||||
end;
|
||||
|
||||
def goForwardToPatrol = \wasBlocked.
|
||||
b <- isBlockedOrFenced;
|
||||
if b {
|
||||
turn left;
|
||||
goForwardToPatrol true;
|
||||
turn right;
|
||||
goForwardToPatrol false;
|
||||
} {
|
||||
if wasBlocked {
|
||||
isLoop <- leaveBreadcrumbs;
|
||||
if isLoop {
|
||||
fail "loop";
|
||||
} {};
|
||||
} {};
|
||||
move;
|
||||
};
|
||||
end;
|
||||
|
||||
/**
|
||||
There should only be one place in the
|
||||
code where an exception is thrown: that is,
|
||||
if a treaded breadcrumb is encountered.
|
||||
*/
|
||||
def checkIsEnclosedInner =
|
||||
try {
|
||||
goForwardToPatrol false;
|
||||
// Water is the outer boundary
|
||||
hasWater <- ishere "water";
|
||||
if hasWater {
|
||||
return false;
|
||||
} {
|
||||
checkIsEnclosedInner;
|
||||
};
|
||||
} {
|
||||
return true;
|
||||
};
|
||||
return b;
|
||||
end;
|
||||
|
||||
def checkIsEnclosed =
|
||||
|
||||
// The "evaporator" drill is used
|
||||
// to clear water tiles.
|
||||
let specialDrill = "evaporator" in
|
||||
create specialDrill;
|
||||
equip specialDrill;
|
||||
|
||||
// **NOTE:** System robots can walk on water
|
||||
// so we only need this if we want to
|
||||
// demo the algorithm with a player robot.
|
||||
// create "boat";
|
||||
// equip "boat";
|
||||
|
||||
checkIsEnclosedInner;
|
||||
maybePath <- path (inL ()) (inR "water");
|
||||
case maybePath (\_. return True) (\_. return False);
|
||||
end;
|
||||
|
||||
def boolToInt = \b. if (b) {return 1} {return 0}; end;
|
||||
@ -238,35 +118,13 @@ objectives:
|
||||
}
|
||||
end;
|
||||
|
||||
/**
|
||||
There are 3 sheep.
|
||||
They have indices 1, 2, 3.
|
||||
(The base has index 0).
|
||||
|
||||
THIS DOES NOT WORK!
|
||||
*/
|
||||
def countSheepWithRecursive = \predicateCmd. \i.
|
||||
|
||||
if (i > 0) {
|
||||
val <- getValForSheepIndex predicateCmd i;
|
||||
recursiveCount <- countSheepWithRecursive predicateCmd $ i - 1;
|
||||
return $ val + recursiveCount;
|
||||
} {
|
||||
return 0;
|
||||
}
|
||||
end;
|
||||
|
||||
|
||||
def countSheepWith = \predicateCmd.
|
||||
|
||||
val1 <- getValForSheepIndex predicateCmd 1;
|
||||
val2 <- getValForSheepIndex predicateCmd 2;
|
||||
val3 <- getValForSheepIndex predicateCmd 3;
|
||||
return $ val1 + val2 + val3;
|
||||
|
||||
end;
|
||||
|
||||
|
||||
justFilledGap <- as base {
|
||||
isStandingOnBridge;
|
||||
};
|
||||
@ -366,6 +224,8 @@ robots:
|
||||
dir: [0, 1]
|
||||
inventory:
|
||||
- [4, wool]
|
||||
unwalkable:
|
||||
- gate
|
||||
program: |
|
||||
run "scenarios/Challenges/Ranching/_gated-paddock/meandering-sheep.sw";
|
||||
entities:
|
||||
|
@ -41,8 +41,10 @@ Achievements
|
||||
1320-world-DSL
|
||||
1356-portals
|
||||
144-subworlds
|
||||
836-pathfinding
|
||||
1341-command-count.yaml
|
||||
1355-combustion.yaml
|
||||
1379-single-world-portal-reorientation.yaml
|
||||
1399-backup-command.yaml
|
||||
1430-built-robot-ownership.yaml
|
||||
1536-custom-unwalkable-entities.yaml
|
||||
|
50
data/scenarios/Testing/1536-custom-unwalkable-entities.yaml
Normal file
50
data/scenarios/Testing/1536-custom-unwalkable-entities.yaml
Normal file
@ -0,0 +1,50 @@
|
||||
version: 1
|
||||
name: Custom unwalkability
|
||||
description: The base robot cannot walk through trees.
|
||||
The scenario shall be failed if the robot
|
||||
manages to walk through the tree by moving
|
||||
three cells to the east.
|
||||
objectives:
|
||||
- goal:
|
||||
- Get the flower
|
||||
condition: |
|
||||
as base {has "flower"};
|
||||
prerequisite:
|
||||
not: has_bitcoin
|
||||
- id: has_bitcoin
|
||||
optional: true
|
||||
goal:
|
||||
- Do not get the bitcoin
|
||||
condition: |
|
||||
as base {has "bitcoin"};
|
||||
solution: |
|
||||
def tryMove = try {move} {}; end;
|
||||
tryMove;
|
||||
tryMove;
|
||||
tryMove;
|
||||
grab;
|
||||
robots:
|
||||
- name: base
|
||||
dir: [1, 0]
|
||||
display:
|
||||
attr: robot
|
||||
devices:
|
||||
- logger
|
||||
- grabber
|
||||
- treads
|
||||
- dictionary
|
||||
- net
|
||||
unwalkable:
|
||||
- tree
|
||||
known: [tree, flower, bitcoin]
|
||||
world:
|
||||
palette:
|
||||
'B': [grass, null, base]
|
||||
'.': [grass]
|
||||
'T': [grass, tree]
|
||||
'b': [grass, bitcoin]
|
||||
'f': [grass, flower]
|
||||
upperleft: [0, 0]
|
||||
map: |
|
||||
BfTb
|
||||
|
8
data/scenarios/Testing/836-pathfinding/00-ORDER.txt
Normal file
8
data/scenarios/Testing/836-pathfinding/00-ORDER.txt
Normal file
@ -0,0 +1,8 @@
|
||||
836-path-exists-find-location.yaml
|
||||
836-path-exists-find-entity.yaml
|
||||
836-path-exists-find-entity-unwalkable.yaml
|
||||
836-path-exists-distance-limit-unreachable.yaml
|
||||
836-path-exists-distance-limit-reachable.yaml
|
||||
836-no-path-exists1.yaml
|
||||
836-no-path-exists2.yaml
|
||||
836-automatic-waypoint-navigation.yaml
|
@ -0,0 +1,154 @@
|
||||
version: 1
|
||||
name: Automatic navigation between waypoints
|
||||
description: |
|
||||
Demonstrate shortest-path patrolling between waypoints
|
||||
creative: false
|
||||
solution: |
|
||||
run "scenarios/Testing/836-pathfinding/_836-automatic-waypoint-navigation/patrol.sw"
|
||||
objectives:
|
||||
- goal:
|
||||
- Collect 64 eggs
|
||||
condition: |
|
||||
as base {
|
||||
eggCount <- count "egg";
|
||||
return $ eggCount >= 64;
|
||||
};
|
||||
attrs:
|
||||
- name: easter_egg
|
||||
fg: "#ffff88"
|
||||
bg: "#eebbff"
|
||||
entities:
|
||||
- name: wayfinder
|
||||
display:
|
||||
char: 'w'
|
||||
description:
|
||||
- Enables `path` command
|
||||
properties: [known, portable]
|
||||
capabilities: [path]
|
||||
- name: atlas
|
||||
display:
|
||||
char: 'a'
|
||||
description:
|
||||
- Enables `waypoint` command
|
||||
properties: [known, portable]
|
||||
capabilities: [waypoint]
|
||||
- name: kudzu
|
||||
display:
|
||||
char: 'k'
|
||||
attr: plant
|
||||
description:
|
||||
- Dense, impassable plant.
|
||||
properties: [known, unwalkable, growable]
|
||||
growth: [30, 50]
|
||||
- name: egg
|
||||
display:
|
||||
char: 'o'
|
||||
attr: easter_egg
|
||||
description:
|
||||
- A colorful egg laid by the rabbit
|
||||
properties: [known, portable]
|
||||
growth: [5, 10]
|
||||
robots:
|
||||
- name: base
|
||||
loc: [0, 0]
|
||||
dir: [1, 0]
|
||||
devices:
|
||||
- ADT calculator
|
||||
- atlas
|
||||
- branch predictor
|
||||
- comparator
|
||||
- compass
|
||||
- dictionary
|
||||
- grabber
|
||||
- logger
|
||||
- net
|
||||
- scanner
|
||||
- treads
|
||||
- wayfinder
|
||||
- name: gardener
|
||||
description: Periodically chops down the kudzu plant
|
||||
dir: [1, 0]
|
||||
system: true
|
||||
devices:
|
||||
- ADT calculator
|
||||
- branch predictor
|
||||
- comparator
|
||||
- compass
|
||||
- dictionary
|
||||
- harvester
|
||||
- treads
|
||||
- logger
|
||||
display:
|
||||
invisible: true
|
||||
program: |
|
||||
run "scenarios/Testing/836-pathfinding/_836-automatic-waypoint-navigation/gardener.sw"
|
||||
- name: rabbit
|
||||
dir: [1, 0]
|
||||
loc: [2, -2]
|
||||
system: true
|
||||
devices:
|
||||
- treads
|
||||
- logger
|
||||
inventory:
|
||||
- [64, egg]
|
||||
display:
|
||||
invisible: false
|
||||
attr: snow
|
||||
char: R
|
||||
program: |
|
||||
run "scenarios/Testing/836-pathfinding/_836-automatic-waypoint-navigation/rabbit.sw"
|
||||
known: [flower, boulder, kudzu]
|
||||
world:
|
||||
upperleft: [-1, 1]
|
||||
palette:
|
||||
'.': [grass]
|
||||
'k': [stone, kudzu, gardener]
|
||||
'┌': [stone, upper left corner]
|
||||
'┐': [stone, upper right corner]
|
||||
'└': [stone, lower left corner]
|
||||
'┘': [stone, lower right corner]
|
||||
'─': [stone, horizontal wall]
|
||||
'│': [stone, vertical wall]
|
||||
structures:
|
||||
- name: cornerbox
|
||||
structure:
|
||||
palette:
|
||||
'.': [dirt]
|
||||
'@': [dirt, boulder]
|
||||
'w':
|
||||
cell: [dirt]
|
||||
waypoint:
|
||||
name: wp
|
||||
map: |
|
||||
@@@
|
||||
@w.
|
||||
@.@
|
||||
placements:
|
||||
- src: cornerbox
|
||||
offset: [2, -2]
|
||||
orient:
|
||||
up: north
|
||||
- src: cornerbox
|
||||
offset: [8, -2]
|
||||
orient:
|
||||
up: east
|
||||
- src: cornerbox
|
||||
offset: [8, -6]
|
||||
orient:
|
||||
up: south
|
||||
- src: cornerbox
|
||||
offset: [2, -6]
|
||||
orient:
|
||||
up: west
|
||||
map: |
|
||||
┌───────────┐
|
||||
│...........│
|
||||
│...........│
|
||||
│.....k.....│
|
||||
│...........│
|
||||
│...........│
|
||||
│...........│
|
||||
│.....k.....│
|
||||
│...........│
|
||||
│...........│
|
||||
└───────────┘
|
@ -0,0 +1,48 @@
|
||||
version: 1
|
||||
name: Builtin pathfinding, unreachable goal, enclosed robot
|
||||
description: |
|
||||
There is finite area to explore, so
|
||||
this will terminate quickly.
|
||||
creative: false
|
||||
objectives:
|
||||
- goal:
|
||||
- Flower must not be reachable.
|
||||
condition: |
|
||||
as base {
|
||||
nextDir <- path (inL ()) (inR "flower");
|
||||
return $ case nextDir (\_. true) (\_. false);
|
||||
};
|
||||
solution: |
|
||||
noop;
|
||||
entities:
|
||||
- name: wayfinder
|
||||
display:
|
||||
char: 'w'
|
||||
description:
|
||||
- Enables `path` command
|
||||
properties: [known, portable]
|
||||
capabilities: [path]
|
||||
robots:
|
||||
- name: base
|
||||
dir: [1,0]
|
||||
devices:
|
||||
- treads
|
||||
- logger
|
||||
- ADT calculator
|
||||
- dictionary
|
||||
- wayfinder
|
||||
known: [mountain, flower, tree]
|
||||
world:
|
||||
palette:
|
||||
'B': [grass, null, base]
|
||||
'.': [grass]
|
||||
'x': [stone, mountain]
|
||||
'f': [grass, flower]
|
||||
'T': [grass, tree]
|
||||
upperleft: [0, 0]
|
||||
map: |
|
||||
xxxxx......
|
||||
x...x......
|
||||
x.B.x...f..
|
||||
x...x......
|
||||
xxxxx......
|
@ -0,0 +1,52 @@
|
||||
version: 1
|
||||
name: Builtin pathfinding, unreachable goal, enclosed goal
|
||||
description: |
|
||||
There is infinite area to explore, so
|
||||
this will fail to terminate unless
|
||||
a limit is set on the max distance.
|
||||
|
||||
In this scenario, we fall back onto the internal distance limit.
|
||||
Normally, it would be very expensive to allow this goal condition
|
||||
check to run upon every tick. But in this case, we should
|
||||
have won the scenario by the first tick.
|
||||
creative: false
|
||||
objectives:
|
||||
- goal:
|
||||
- Flower must not be reachable.
|
||||
condition: |
|
||||
as base {
|
||||
nextDir <- path (inL ()) (inR "flower");
|
||||
return $ case nextDir (\_. true) (\_. false);
|
||||
};
|
||||
solution: |
|
||||
noop;
|
||||
entities:
|
||||
- name: wayfinder
|
||||
display:
|
||||
char: 'w'
|
||||
description:
|
||||
- Enables `path` command
|
||||
properties: [known, portable]
|
||||
capabilities: [path]
|
||||
robots:
|
||||
- name: base
|
||||
dir: [1,0]
|
||||
devices:
|
||||
- treads
|
||||
- logger
|
||||
- ADT calculator
|
||||
- dictionary
|
||||
- wayfinder
|
||||
world:
|
||||
palette:
|
||||
'B': [grass, null, base]
|
||||
'.': [grass]
|
||||
'x': [stone, mountain]
|
||||
'f': [grass, flower]
|
||||
upperleft: [0, 0]
|
||||
map: |
|
||||
......xxxxx
|
||||
......x...x
|
||||
..B...x.f.x
|
||||
......x...x
|
||||
......xxxxx
|
@ -0,0 +1,33 @@
|
||||
version: 1
|
||||
name: Builtin pathfinding - entity target
|
||||
description: |
|
||||
Use distance limit with `path` command
|
||||
creative: false
|
||||
objectives:
|
||||
- goal:
|
||||
- Flower must be reachable within 3 cells.
|
||||
condition: |
|
||||
as base {
|
||||
nextDir <- path (inR 3) (inR "flower");
|
||||
return $ case nextDir (\_. false) (\_. true);
|
||||
};
|
||||
solution: |
|
||||
move;
|
||||
robots:
|
||||
- name: base
|
||||
dir: [1,0]
|
||||
devices:
|
||||
- treads
|
||||
- logger
|
||||
- dictionary
|
||||
- grabber
|
||||
world:
|
||||
palette:
|
||||
'B': [grass, null, base]
|
||||
'.': [grass]
|
||||
'f': [grass, flower]
|
||||
upperleft: [0, 0]
|
||||
map: |
|
||||
.......
|
||||
.B...f.
|
||||
.......
|
@ -0,0 +1,35 @@
|
||||
version: 1
|
||||
name: Builtin pathfinding - entity target
|
||||
description: |
|
||||
Use distance limit with `path` command
|
||||
creative: false
|
||||
objectives:
|
||||
- goal:
|
||||
- Flower must NOT be reachable within 3 cells.
|
||||
condition: |
|
||||
as base {
|
||||
nextDir <- path (inR 3) (inR "flower");
|
||||
return $ case nextDir (\_. true) (\_. false);
|
||||
};
|
||||
solution: |
|
||||
turn back;
|
||||
move;
|
||||
robots:
|
||||
- name: base
|
||||
dir: [1,0]
|
||||
devices:
|
||||
- treads
|
||||
- logger
|
||||
- dictionary
|
||||
- grabber
|
||||
world:
|
||||
palette:
|
||||
'B': [grass, null, base]
|
||||
'.': [grass]
|
||||
'f': [grass, flower]
|
||||
upperleft: [0, 0]
|
||||
map: |
|
||||
.......
|
||||
..B..f.
|
||||
.......
|
||||
|
@ -0,0 +1,51 @@
|
||||
version: 1
|
||||
name: Builtin pathfinding - entity target
|
||||
description: |
|
||||
Use `path` command to navigate to an entity
|
||||
creative: false
|
||||
objectives:
|
||||
- goal:
|
||||
- Reach and face the water.
|
||||
condition: |
|
||||
as base {
|
||||
itemAhead <- scan forward;
|
||||
return $ case itemAhead (\_. false) (\item. item == "water");
|
||||
};
|
||||
solution: |
|
||||
run "scenarios/Testing/836-pathfinding/_836-path-exists/find-entity-unwalkable-solution.sw";
|
||||
entities:
|
||||
- name: wayfinder
|
||||
display:
|
||||
char: 'w'
|
||||
description:
|
||||
- Enables `path` command
|
||||
properties: [known, portable]
|
||||
capabilities: [path]
|
||||
robots:
|
||||
- name: base
|
||||
dir: [1,0]
|
||||
devices:
|
||||
- ADT calculator
|
||||
- branch predictor
|
||||
- comparator
|
||||
- compass
|
||||
- dictionary
|
||||
- logger
|
||||
- grabber
|
||||
- scanner
|
||||
- treads
|
||||
- wayfinder
|
||||
known: [water, boulder]
|
||||
world:
|
||||
dsl: |
|
||||
{blank, boulder}
|
||||
palette:
|
||||
'B': [grass, erase, base]
|
||||
'.': [grass, erase]
|
||||
'x': [stone, boulder]
|
||||
'w': [grass, water]
|
||||
upperleft: [0, 0]
|
||||
map: |
|
||||
Bx...
|
||||
.x.x.
|
||||
...xw
|
@ -0,0 +1,47 @@
|
||||
version: 1
|
||||
name: Builtin pathfinding - entity target
|
||||
description: |
|
||||
Use `path` command to navigate to an entity
|
||||
creative: false
|
||||
objectives:
|
||||
- goal:
|
||||
- Get the flower.
|
||||
condition: |
|
||||
as base {has "flower";}
|
||||
solution: |
|
||||
run "scenarios/Testing/836-pathfinding/_836-path-exists/find-entity-solution.sw";
|
||||
entities:
|
||||
- name: wayfinder
|
||||
display:
|
||||
char: 'w'
|
||||
description:
|
||||
- Enables `path` command
|
||||
properties: [known, portable]
|
||||
capabilities: [path]
|
||||
robots:
|
||||
- name: base
|
||||
dir: [1,0]
|
||||
devices:
|
||||
- ADT calculator
|
||||
- branch predictor
|
||||
- comparator
|
||||
- compass
|
||||
- dictionary
|
||||
- logger
|
||||
- grabber
|
||||
- treads
|
||||
- wayfinder
|
||||
known: [flower, mountain]
|
||||
world:
|
||||
palette:
|
||||
'B': [grass, null, base]
|
||||
'.': [grass]
|
||||
'x': [stone, mountain]
|
||||
'f': [grass, flower]
|
||||
upperleft: [0, 0]
|
||||
map: |
|
||||
xxxxfx
|
||||
x.Bx.x
|
||||
x.xx.x
|
||||
x....x
|
||||
xxxxxx
|
@ -0,0 +1,47 @@
|
||||
version: 1
|
||||
name: Builtin pathfinding - location target
|
||||
description: |
|
||||
Use `path` command to navigate to a location
|
||||
creative: false
|
||||
objectives:
|
||||
- goal:
|
||||
- Get the flower.
|
||||
condition: |
|
||||
as base {has "flower";}
|
||||
solution: |
|
||||
run "scenarios/Testing/836-pathfinding/_836-path-exists/find-location-solution.sw";
|
||||
entities:
|
||||
- name: wayfinder
|
||||
display:
|
||||
char: 'w'
|
||||
description:
|
||||
- Enables `path` command
|
||||
properties: [known, portable]
|
||||
capabilities: [path]
|
||||
robots:
|
||||
- name: base
|
||||
dir: [1,0]
|
||||
devices:
|
||||
- ADT calculator
|
||||
- branch predictor
|
||||
- comparator
|
||||
- compass
|
||||
- dictionary
|
||||
- logger
|
||||
- grabber
|
||||
- treads
|
||||
- wayfinder
|
||||
known: [flower, mountain]
|
||||
world:
|
||||
palette:
|
||||
'B': [grass, null, base]
|
||||
'.': [grass]
|
||||
'x': [stone, mountain]
|
||||
'f': [grass, flower]
|
||||
upperleft: [0, 0]
|
||||
map: |
|
||||
xxxxfx
|
||||
x.Bx.x
|
||||
x.xx.x
|
||||
x....x
|
||||
xxxxxx
|
@ -0,0 +1,18 @@
|
||||
def harvestPlant =
|
||||
emptyHere <- isempty;
|
||||
if emptyHere {
|
||||
watch down;
|
||||
wait 1000;
|
||||
} {
|
||||
wait 50;
|
||||
harvest;
|
||||
return ();
|
||||
};
|
||||
end;
|
||||
|
||||
def go =
|
||||
harvestPlant;
|
||||
go;
|
||||
end;
|
||||
|
||||
go;
|
@ -0,0 +1,35 @@
|
||||
def goDir = \f. \d.
|
||||
if (d == down) {
|
||||
eggHere <- ishere "egg";
|
||||
if eggHere {grab; return ()} {};
|
||||
return ()
|
||||
} {
|
||||
turn d;
|
||||
|
||||
// An obstruction might arise after
|
||||
// navigation direction is determined
|
||||
// but before we move.
|
||||
try {
|
||||
move;
|
||||
} {};
|
||||
f;
|
||||
}
|
||||
end;
|
||||
|
||||
def followRoute = \loc.
|
||||
nextDir <- path (inL ()) (inL loc);
|
||||
case nextDir return $ goDir $ followRoute loc;
|
||||
end;
|
||||
|
||||
def visitNextWaypoint = \nextWpIdx.
|
||||
nextWaypointQuery <- waypoint "wp" nextWpIdx;
|
||||
followRoute $ snd nextWaypointQuery;
|
||||
|
||||
visitNextWaypoint $ nextWpIdx + 1;
|
||||
end;
|
||||
|
||||
def go =
|
||||
visitNextWaypoint 0;
|
||||
end;
|
||||
|
||||
go;
|
@ -0,0 +1,16 @@
|
||||
|
||||
def visitNextWaypoint = \nextWpIdx.
|
||||
emptyHere <- isempty;
|
||||
if emptyHere {
|
||||
try {
|
||||
place "egg";
|
||||
} {};
|
||||
} {};
|
||||
watch down;
|
||||
nextWaypointQuery <- waypoint "wp" nextWpIdx;
|
||||
teleport self $ snd nextWaypointQuery;
|
||||
wait 1000;
|
||||
visitNextWaypoint $ nextWpIdx + 1;
|
||||
end;
|
||||
|
||||
visitNextWaypoint 0;
|
@ -0,0 +1,14 @@
|
||||
def goDir = \f. \d.
|
||||
if (d == down) {
|
||||
grab; return ()
|
||||
} {
|
||||
turn d; move; f;
|
||||
}
|
||||
end;
|
||||
|
||||
def followRoute =
|
||||
nextDir <- path (inL ()) (inR "flower");
|
||||
case nextDir return $ goDir followRoute;
|
||||
end;
|
||||
|
||||
followRoute;
|
@ -0,0 +1,22 @@
|
||||
def goDir = \goalItem. \f. \d.
|
||||
if (d == down) {
|
||||
grab; return ()
|
||||
} {
|
||||
turn d;
|
||||
itemAhead <- scan forward;
|
||||
let isGoalAhead = case itemAhead (\_. false) (\item. item == goalItem) in
|
||||
if isGoalAhead {
|
||||
return ();
|
||||
} {
|
||||
move; f;
|
||||
};
|
||||
}
|
||||
end;
|
||||
|
||||
def followRoute =
|
||||
let goalItem = "water" in
|
||||
nextDir <- path (inL ()) (inR goalItem);
|
||||
case nextDir return $ goDir goalItem followRoute;
|
||||
end;
|
||||
|
||||
followRoute;
|
@ -0,0 +1,14 @@
|
||||
def goDir = \f. \d.
|
||||
if (d == down) {
|
||||
grab; return ()
|
||||
} {
|
||||
turn d; move; f;
|
||||
}
|
||||
end;
|
||||
|
||||
def followRoute =
|
||||
nextDir <- path (inL ()) (inL (4, 0));
|
||||
case nextDir return $ goDir followRoute;
|
||||
end;
|
||||
|
||||
followRoute;
|
@ -11,9 +11,8 @@
|
||||
"description": "The name of the robot. This shows up in the list of robots in the game (F2), and is also how the robot will be referred to in the world palette."
|
||||
},
|
||||
"description": {
|
||||
"default": [],
|
||||
"type": "string",
|
||||
"description": "A description of the robot, given as a list of paragraphs. This is currently not used for much (perhaps not at all?)."
|
||||
"description": "A description of the robot. This is currently not used for much, other than scenario documentation."
|
||||
},
|
||||
"loc": {
|
||||
"description": "An optional starting location for the robot. If the loc field is specified, then a concrete robot will be created at the given location. If this field is omitted, then this robot record exists only as a template which can be referenced from a cell in the world palette. Concrete robots will then be created wherever the corresponding palette character is used in the world map.",
|
||||
@ -69,6 +68,14 @@
|
||||
"default": false,
|
||||
"type": "boolean",
|
||||
"description": "Whether the robot is heavy. Heavy robots require tank treads to move (rather than just treads for other robots)."
|
||||
},
|
||||
"unwalkable": {
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "A list of entities that this robot cannot walk across."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -55,6 +55,7 @@
|
||||
"selfdestruct"
|
||||
"move"
|
||||
"backup"
|
||||
"path"
|
||||
"push"
|
||||
"stride"
|
||||
"turn"
|
||||
|
@ -1,6 +1,6 @@
|
||||
syn keyword Keyword def end let in require
|
||||
syn keyword Builtins self parent base if inl inr case fst snd force undefined fail not format chars split charat tochar key
|
||||
syn keyword Command noop wait selfdestruct move backup push stride turn grab harvest ignite place give equip unequip make has equipped count drill use build salvage reprogram say listen log view appear create halt time scout whereami waypoint detect resonate density sniff chirp watch surveil heading blocked scan upload ishere isempty meet meetall whoami setname random run return try swap atomic instant installkeyhandler teleport as robotnamed robotnumbered knows
|
||||
syn keyword Command noop wait selfdestruct move backup path push stride turn grab harvest ignite place give equip unequip make has equipped count drill use build salvage reprogram say listen log view appear create halt time scout whereami waypoint detect resonate density sniff chirp watch surveil heading blocked scan upload ishere isempty meet meetall whoami setname random run return try swap atomic instant installkeyhandler teleport as robotnamed robotnumbered knows
|
||||
syn keyword Direction east north west south down forward left back right
|
||||
syn keyword Type int text dir bool cmd void unit actor
|
||||
|
||||
|
@ -58,7 +58,7 @@
|
||||
},
|
||||
{
|
||||
"name": "keyword.other",
|
||||
"match": "\\b(?i)(self|parent|base|if|inl|inr|case|fst|snd|force|undefined|fail|not|format|chars|split|charat|tochar|key|noop|wait|selfdestruct|move|backup|push|stride|turn|grab|harvest|ignite|place|give|equip|unequip|make|has|equipped|count|drill|use|build|salvage|reprogram|say|listen|log|view|appear|create|halt|time|scout|whereami|waypoint|detect|resonate|density|sniff|chirp|watch|surveil|heading|blocked|scan|upload|ishere|isempty|meet|meetall|whoami|setname|random|run|return|try|swap|atomic|instant|installkeyhandler|teleport|as|robotnamed|robotnumbered|knows)\\b"
|
||||
"match": "\\b(?i)(self|parent|base|if|inl|inr|case|fst|snd|force|undefined|fail|not|format|chars|split|charat|tochar|key|noop|wait|selfdestruct|move|backup|path|push|stride|turn|grab|harvest|ignite|place|give|equip|unequip|make|has|equipped|count|drill|use|build|salvage|reprogram|say|listen|log|view|appear|create|halt|time|scout|whereami|waypoint|detect|resonate|density|sniff|chirp|watch|surveil|heading|blocked|scan|upload|ishere|isempty|meet|meetall|whoami|setname|random|run|return|try|swap|atomic|instant|installkeyhandler|teleport|as|robotnamed|robotnumbered|knows)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -35,6 +35,7 @@ module Swarm.Game.Robot (
|
||||
robotEntity,
|
||||
robotName,
|
||||
trobotName,
|
||||
unwalkableEntities,
|
||||
robotCreatedAt,
|
||||
robotDisplay,
|
||||
robotLocation,
|
||||
@ -270,6 +271,7 @@ data RobotR (phase :: RobotPhase) = RobotR
|
||||
, _selfDestruct :: Bool
|
||||
, _activityCounts :: ActivityCounts
|
||||
, _runningAtomic :: Bool
|
||||
, _unwalkableEntities :: Set EntityName
|
||||
, _robotCreatedAt :: TimeSpec
|
||||
}
|
||||
deriving (Generic)
|
||||
@ -310,11 +312,15 @@ instance ToSample Robot where
|
||||
-- . 'entityName'@.
|
||||
robotEntity :: Lens' (RobotR phase) Entity
|
||||
|
||||
-- | Entities that the robot cannot move onto
|
||||
unwalkableEntities :: Lens' Robot (Set EntityName)
|
||||
|
||||
-- | The creation date of the robot.
|
||||
robotCreatedAt :: Lens' Robot TimeSpec
|
||||
|
||||
-- robotName and trobotName could be generalized to robotName' ::
|
||||
-- Lens' (RobotR phase) Text. However, type inference does not work
|
||||
-- robotName and trobotName could be generalized to
|
||||
-- @robotName' :: Lens' (RobotR phase) Text@.
|
||||
-- However, type inference does not work
|
||||
-- very well with the polymorphic version, so we export both
|
||||
-- monomorphic versions instead.
|
||||
|
||||
@ -499,10 +505,12 @@ mkRobot ::
|
||||
Bool ->
|
||||
-- | Is this robot heavy?
|
||||
Bool ->
|
||||
-- | Unwalkable entities
|
||||
Set EntityName ->
|
||||
-- | Creation date
|
||||
TimeSpec ->
|
||||
RobotR phase
|
||||
mkRobot rid pid name descr loc dir disp m devs inv sys heavy ts =
|
||||
mkRobot rid pid name descr loc dir disp m devs inv sys heavy unwalkables ts =
|
||||
RobotR
|
||||
{ _robotEntity =
|
||||
mkEntity disp name descr [] []
|
||||
@ -532,6 +540,7 @@ mkRobot rid pid name descr loc dir disp m devs inv sys heavy ts =
|
||||
_activityWindow = mkWindow 64
|
||||
}
|
||||
, _runningAtomic = False
|
||||
, _unwalkableEntities = unwalkables
|
||||
}
|
||||
where
|
||||
inst = fromList devs
|
||||
@ -557,6 +566,7 @@ instance FromJSONE EntityMap TRobot where
|
||||
<*> v ..:? "inventory" ..!= []
|
||||
<*> pure sys
|
||||
<*> liftE (v .:? "heavy" .!= False)
|
||||
<*> liftE (v .:? "unwalkable" ..!= mempty)
|
||||
<*> pure 0
|
||||
where
|
||||
mkMachine Nothing = Out VUnit emptyStore []
|
||||
|
@ -76,6 +76,7 @@ import Swarm.Game.Scenario.Topography.Navigation.Portal (Navigation (..), destin
|
||||
import Swarm.Game.Scenario.Topography.Navigation.Waypoint (WaypointName (..))
|
||||
import Swarm.Game.State
|
||||
import Swarm.Game.Step.Combustion qualified as Combustion
|
||||
import Swarm.Game.Step.Pathfinding
|
||||
import Swarm.Game.Step.Util
|
||||
import Swarm.Game.Universe
|
||||
import Swarm.Game.Value
|
||||
@ -392,6 +393,7 @@ hypotheticalRobot c =
|
||||
[]
|
||||
True
|
||||
False
|
||||
mempty
|
||||
|
||||
evaluateCESK ::
|
||||
(Has (Lift IO) sig m, Has (Throw Exn) sig m, Has (State GameState) sig m) =>
|
||||
@ -995,6 +997,7 @@ addSeedBot e (minT, maxT) loc ts =
|
||||
[(1, e)]
|
||||
True
|
||||
False
|
||||
mempty
|
||||
ts
|
||||
|
||||
-- | Interpret the execution (or evaluation) of a constant application
|
||||
@ -1038,6 +1041,29 @@ execConst c vs s k = do
|
||||
Backup -> do
|
||||
orient <- use robotOrientation
|
||||
moveInDirection $ applyTurn (DRelative $ DPlanar DBack) $ orient ? zero
|
||||
Path -> case vs of
|
||||
[VInj hasLimit limitVal, VInj findEntity goalVal] -> do
|
||||
maybeLimit <-
|
||||
if hasLimit
|
||||
then case limitVal of
|
||||
VInt d -> return $ Just d
|
||||
_ -> badConst
|
||||
else return Nothing
|
||||
goal <-
|
||||
if findEntity
|
||||
then case goalVal of
|
||||
VText eName -> return $ EntityTarget eName
|
||||
_ -> badConst
|
||||
else case goalVal of
|
||||
VPair (VInt x) (VInt y) ->
|
||||
return $
|
||||
LocationTarget $
|
||||
Location (fromIntegral x) (fromIntegral y)
|
||||
_ -> badConst
|
||||
robotLoc <- use robotLocation
|
||||
result <- pathCommand maybeLimit robotLoc goal
|
||||
return $ Out (asValue result) s k
|
||||
_ -> badConst
|
||||
Push -> do
|
||||
-- Figure out where we're going
|
||||
loc <- use robotLocation
|
||||
@ -1876,6 +1902,7 @@ execConst c vs s k = do
|
||||
[]
|
||||
isSystemRobot
|
||||
False
|
||||
mempty
|
||||
createdAt
|
||||
|
||||
-- Provision the new robot with the necessary devices and inventory.
|
||||
@ -2384,26 +2411,6 @@ execConst c vs s k = do
|
||||
updateRobotLocation loc nextLoc
|
||||
return $ Out VUnit s k
|
||||
|
||||
-- Make sure nothing is in the way. Note that system robots implicitly ignore
|
||||
-- and base throws on failure.
|
||||
checkMoveFailure :: HasRobotStepState sig m => Cosmic Location -> m (Maybe MoveFailureDetails)
|
||||
checkMoveFailure nextLoc = do
|
||||
me <- entityAt nextLoc
|
||||
systemRob <- use systemRobot
|
||||
caps <- use robotCapabilities
|
||||
return $ do
|
||||
e <- me
|
||||
guard $ not systemRob
|
||||
go caps e
|
||||
where
|
||||
go caps e
|
||||
-- robots can not walk through walls
|
||||
| e `hasProperty` Unwalkable = Just $ MoveFailureDetails e PathBlocked
|
||||
-- robots drown if they walk over liquid without boat
|
||||
| e `hasProperty` Liquid && CFloat `S.notMember` caps =
|
||||
Just $ MoveFailureDetails e PathLiquid
|
||||
| otherwise = Nothing
|
||||
|
||||
applyMoveFailureEffect ::
|
||||
(HasRobotStepState sig m, Has (Lift IO) sig m) =>
|
||||
Maybe MoveFailureDetails ->
|
||||
@ -2582,9 +2589,6 @@ grantAchievement a = do
|
||||
a
|
||||
(Attainment (GameplayAchievement a) scenarioPath currentTime)
|
||||
|
||||
data MoveFailureMode = PathBlocked | PathLiquid
|
||||
data MoveFailureDetails = MoveFailureDetails Entity MoveFailureMode
|
||||
|
||||
-- | How to handle failure, for example when moving to blocked location
|
||||
data RobotFailure = ThrowExn | Destroy | IgnoreFail
|
||||
|
||||
|
@ -61,9 +61,7 @@ igniteCommand c d = do
|
||||
let selfCombustibility = (e ^. entityCombustion) ? defaultCombustibility
|
||||
createdAt <- getNow
|
||||
combustionDurationRand <- addCombustionBot e selfCombustibility createdAt loc
|
||||
|
||||
let neighborLocs = map (offsetBy loc . flip applyTurn north . DRelative . DPlanar) listEnums
|
||||
forM_ neighborLocs $ igniteNeighbor createdAt combustionDurationRand
|
||||
forM_ (getNeighborLocs loc) $ igniteNeighbor createdAt combustionDurationRand
|
||||
where
|
||||
verb = "ignite"
|
||||
verbed = "ignited"
|
||||
@ -111,6 +109,7 @@ addCombustionBot inputEntity combustibility ts loc = do
|
||||
botInventory
|
||||
True
|
||||
False
|
||||
mempty
|
||||
ts
|
||||
return combustionDurationRand
|
||||
where
|
||||
@ -224,4 +223,5 @@ addIgnitionBot ignitionDelay inputEntity ts loc =
|
||||
[]
|
||||
True
|
||||
False
|
||||
mempty
|
||||
ts
|
||||
|
137
src/Swarm/Game/Step/Pathfinding.hs
Normal file
137
src/Swarm/Game/Step/Pathfinding.hs
Normal file
@ -0,0 +1,137 @@
|
||||
-- |
|
||||
-- SPDX-License-Identifier: BSD-3-Clause
|
||||
--
|
||||
-- Implementation of the @path@ command for robots.
|
||||
--
|
||||
-- = Design considerations
|
||||
-- One possible design of the @path@ command entailed storing a computed
|
||||
-- shortest path and providing a mechanism to retrieve parts of it later
|
||||
-- without recomputing the whole thing.
|
||||
-- However, in general the playfield can be dynamic and obstructions may
|
||||
-- appear that invalidate a given computed shortest path.
|
||||
-- Therefore, there can be limited value in caching a computed path for use
|
||||
-- across ticks.
|
||||
--
|
||||
-- Instead, in the current implementation a complete path is computed
|
||||
-- internally upon invoking the @path@ command, and just the direction of the
|
||||
-- first "move" along that path is returned as a result to the caller.
|
||||
--
|
||||
-- == Max distance
|
||||
--
|
||||
-- We allow the caller to supply a max distance, but also impose an internal maximum
|
||||
-- distance to prevent programming errors from irrecoverably freezing the game.
|
||||
module Swarm.Game.Step.Pathfinding where
|
||||
|
||||
import Control.Carrier.State.Lazy
|
||||
import Control.Effect.Lens
|
||||
import Control.Monad (filterM, guard)
|
||||
import Control.Monad.Trans.Class (lift)
|
||||
import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)
|
||||
import Data.Graph.AStar (aStarM)
|
||||
import Data.HashSet (HashSet)
|
||||
import Data.HashSet qualified as HashSet
|
||||
import Data.Int (Int32)
|
||||
import Swarm.Game.Entity
|
||||
import Swarm.Game.Location
|
||||
import Swarm.Game.State
|
||||
import Swarm.Game.Step.Util
|
||||
import Swarm.Game.Universe
|
||||
import Swarm.Language.Syntax
|
||||
import Swarm.Util (hoistMaybe)
|
||||
|
||||
-- | Shortest paths can either be computed to the nearest entity of
|
||||
-- a given type or to a specific location.
|
||||
data PathfindingTarget
|
||||
= LocationTarget Location
|
||||
| -- | Note: navigation to entities does not benefit from the
|
||||
-- distance heuristic optimization of the A* algorithm.
|
||||
EntityTarget EntityName
|
||||
|
||||
-- | swarm command arguments are converted to idiomatic Haskell
|
||||
-- types before invoking this function, and conversely the callsite
|
||||
-- is also responsible for translating the output type to a swarm value.
|
||||
--
|
||||
-- The cost function is uniformly @1@ between adjacent cells.
|
||||
--
|
||||
-- Viable paths are determined by walkability.
|
||||
-- If the goal type is an Entity, than it is permissible for that
|
||||
-- entity to be 'Unwalkable'.
|
||||
pathCommand ::
|
||||
(HasRobotStepState sig m, Has (State GameState) sig m) =>
|
||||
-- | Distance limit
|
||||
Maybe Integer ->
|
||||
-- | Starting location
|
||||
Cosmic Location ->
|
||||
-- | Search goal
|
||||
PathfindingTarget ->
|
||||
m (Maybe Direction)
|
||||
pathCommand maybeLimit (Cosmic currentSubworld robotLoc) target = do
|
||||
-- This is a short-circuiting optimization; if the goal itself
|
||||
-- is not a walkable cell, then no amount of searching will reach it.
|
||||
isGoalLocWalkable <- case target of
|
||||
LocationTarget loc -> null <$> checkMoveFailure (Cosmic currentSubworld loc)
|
||||
EntityTarget _ -> return True
|
||||
|
||||
runMaybeT $ do
|
||||
guard isGoalLocWalkable
|
||||
maybeFoundPath <- lift computePath
|
||||
foundPath <- hoistMaybe maybeFoundPath
|
||||
return $ nextDir foundPath
|
||||
where
|
||||
computePath =
|
||||
aStarM
|
||||
(neighborFunc withinDistanceLimit . Cosmic currentSubworld)
|
||||
(const $ const $ return 1)
|
||||
(return . distHeuristic)
|
||||
goalReachedFunc
|
||||
(return robotLoc)
|
||||
|
||||
withinDistanceLimit :: Location -> Bool
|
||||
withinDistanceLimit = (<= distanceLimit) . fromIntegral . manhattan robotLoc
|
||||
|
||||
-- Extracts the head of the found path to determine
|
||||
-- the next direction for the robot to proceed along
|
||||
nextDir :: [Location] -> Direction
|
||||
nextDir pathLocs = case pathLocs of
|
||||
[] -> DRelative DDown
|
||||
(nextLoc : _) -> DAbsolute $ nearestDirection $ nextLoc .-. robotLoc
|
||||
|
||||
neighborFunc ::
|
||||
HasRobotStepState sig m =>
|
||||
(Location -> Bool) ->
|
||||
Cosmic Location ->
|
||||
m (HashSet Location)
|
||||
neighborFunc isWithinRange loc = do
|
||||
locs <- filterM isWalkableLoc neighborLocs
|
||||
return $ HashSet.fromList $ map (view planar) locs
|
||||
where
|
||||
neighborLocs = getNeighborLocs loc
|
||||
isWalkableLoc someLoc =
|
||||
if not $ isWithinRange $ view planar someLoc
|
||||
then return False
|
||||
else do
|
||||
isGoal <- goalReachedFunc $ view planar someLoc
|
||||
if isGoal
|
||||
then return True
|
||||
else null <$> checkMoveFailureUnprivileged someLoc
|
||||
|
||||
-- This is an optimization for when a specific location
|
||||
-- is given as the target.
|
||||
-- However, it is not strictly necessary, and in fact
|
||||
-- cannot be used when the target is a certain type of
|
||||
-- entity.
|
||||
distHeuristic :: Location -> Int32
|
||||
distHeuristic = case target of
|
||||
LocationTarget gLoc -> manhattan gLoc
|
||||
EntityTarget _eName -> const 0
|
||||
|
||||
goalReachedFunc :: Has (State GameState) sig m => Location -> m Bool
|
||||
goalReachedFunc loc = case target of
|
||||
LocationTarget gLoc -> return $ loc == gLoc
|
||||
EntityTarget eName -> do
|
||||
me <- entityAt $ Cosmic currentSubworld loc
|
||||
return $ (view entityName <$> me) == Just eName
|
||||
|
||||
-- A failsafe limit is hardcoded to prevent the game from freezing
|
||||
-- if an error exists in some .sw code.
|
||||
distanceLimit = maybe maxPathRange (min maxPathRange) maybeLimit
|
@ -13,7 +13,9 @@ import Control.Effect.Error
|
||||
import Control.Effect.Lens
|
||||
import Control.Effect.Lift
|
||||
import Control.Lens as Lens hiding (Const, distrib, from, parts, use, uses, view, (%=), (+=), (.=), (<+=), (<>=))
|
||||
import Control.Monad (forM, join, when)
|
||||
import Control.Monad (forM, guard, join, when)
|
||||
import Control.Monad.Trans.Class (lift)
|
||||
import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)
|
||||
import Data.Array (bounds, (!))
|
||||
import Data.IntMap qualified as IM
|
||||
import Data.List (find)
|
||||
@ -73,13 +75,13 @@ updateEntityAt cLoc@(Cosmic subworldName loc) upd = do
|
||||
when didChange $
|
||||
wakeWatchingRobots cLoc
|
||||
|
||||
-- * Capabilities
|
||||
|
||||
-- | Exempts the robot from various command constraints
|
||||
-- when it is either a system robot or playing in creative mode
|
||||
isPrivilegedBot :: (Has (State GameState) sig m, Has (State Robot) sig m) => m Bool
|
||||
isPrivilegedBot = (||) <$> use systemRobot <*> use creativeMode
|
||||
|
||||
-- * Exceptions
|
||||
|
||||
-- | Test whether the current robot has a given capability (either
|
||||
-- because it has a device which gives it that capability, or it is a
|
||||
-- system robot, or we are in creative mode).
|
||||
@ -97,6 +99,8 @@ hasCapabilityFor cap term = do
|
||||
h <- hasCapability cap
|
||||
h `holdsOr` Incapable FixByEquip (R.singletonCap cap) term
|
||||
|
||||
-- * Exceptions
|
||||
|
||||
holdsOrFail' :: (Has (Throw Exn) sig m) => Const -> Bool -> [Text] -> m ()
|
||||
holdsOrFail' c a ts = a `holdsOr` cmdExn c ts
|
||||
|
||||
@ -107,17 +111,20 @@ isJustOrFail' c a ts = a `isJustOr` cmdExn c ts
|
||||
cmdExn :: Const -> [Text] -> Exn
|
||||
cmdExn c parts = CmdFailed c (T.unwords parts) Nothing
|
||||
|
||||
-- * Some utility functions
|
||||
|
||||
getNow :: Has (Lift IO) sig m => m TimeSpec
|
||||
getNow = sendIO $ System.Clock.getTime System.Clock.Monotonic
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Some utility functions
|
||||
------------------------------------------------------------
|
||||
|
||||
-- | Set a flag telling the UI that the world needs to be redrawn.
|
||||
flagRedraw :: (Has (State GameState) sig m) => m ()
|
||||
flagRedraw = needsRedraw .= True
|
||||
|
||||
-- * World queries
|
||||
|
||||
getNeighborLocs :: Cosmic Location -> [Cosmic Location]
|
||||
getNeighborLocs loc = map (offsetBy loc . flip applyTurn north . DRelative . DPlanar) listEnums
|
||||
|
||||
-- | Perform an action requiring a 'W.World' state component in a
|
||||
-- larger context with a 'GameState'.
|
||||
zoomWorld ::
|
||||
@ -145,6 +152,8 @@ robotWithID rid = use (robotMap . at rid)
|
||||
robotWithName :: (Has (State GameState) sig m) => Text -> m (Maybe Robot)
|
||||
robotWithName rname = use (robotMap . to IM.elems . to (find $ \r -> r ^. robotName == rname))
|
||||
|
||||
-- * Randomness
|
||||
|
||||
-- | Generate a uniformly random number using the random generator in
|
||||
-- the game state.
|
||||
uniform :: (Has (State GameState) sig m, UniformRange a) => (a, a) -> m a
|
||||
@ -179,3 +188,37 @@ randomName = do
|
||||
i <- uniform (bounds adjs)
|
||||
j <- uniform (bounds names)
|
||||
return $ T.concat [adjs ! i, "_", names ! j]
|
||||
|
||||
-- * Moving
|
||||
|
||||
data MoveFailureMode = PathBlocked | PathLiquid
|
||||
data MoveFailureDetails = MoveFailureDetails Entity MoveFailureMode
|
||||
|
||||
-- | Make sure nothing is in the way.
|
||||
-- No exception for system robots
|
||||
checkMoveFailureUnprivileged :: HasRobotStepState sig m => Cosmic Location -> m (Maybe MoveFailureDetails)
|
||||
checkMoveFailureUnprivileged nextLoc = do
|
||||
me <- entityAt nextLoc
|
||||
caps <- use robotCapabilities
|
||||
unwalkables <- use unwalkableEntities
|
||||
return $ do
|
||||
e <- me
|
||||
go caps unwalkables e
|
||||
where
|
||||
go caps unwalkables e
|
||||
-- robots can not walk through walls
|
||||
| e `hasProperty` Unwalkable || (e ^. entityName) `S.member` unwalkables = Just $ MoveFailureDetails e PathBlocked
|
||||
-- robots drown if they walk over liquid without boat
|
||||
| e `hasProperty` Liquid && CFloat `S.notMember` caps =
|
||||
Just $ MoveFailureDetails e PathLiquid
|
||||
| otherwise = Nothing
|
||||
|
||||
-- | Make sure nothing is in the way. Note that system robots implicitly ignore
|
||||
-- and base throws on failure.
|
||||
checkMoveFailure :: HasRobotStepState sig m => Cosmic Location -> m (Maybe MoveFailureDetails)
|
||||
checkMoveFailure nextLoc = do
|
||||
systemRob <- use systemRobot
|
||||
runMaybeT $ do
|
||||
guard $ not systemRob
|
||||
maybeMoveFailure <- lift $ checkMoveFailureUnprivileged nextLoc
|
||||
hoistMaybe maybeMoveFailure
|
||||
|
@ -14,6 +14,7 @@ import Linear (V2 (..))
|
||||
import Swarm.Game.Entity
|
||||
import Swarm.Game.Location
|
||||
import Swarm.Game.Robot
|
||||
import Swarm.Language.Direction
|
||||
import Swarm.Language.Value
|
||||
|
||||
-- * Patterns
|
||||
@ -53,6 +54,9 @@ instance Valuable Entity where
|
||||
instance Valuable Robot where
|
||||
asValue = VRobot . view robotID
|
||||
|
||||
instance Valuable Direction where
|
||||
asValue = VDir
|
||||
|
||||
instance (Valuable a) => Valuable (Maybe a) where
|
||||
asValue Nothing = VInj False VUnit
|
||||
asValue (Just x) = VInj True $ asValue x
|
||||
|
@ -36,6 +36,8 @@ data Capability
|
||||
CMove
|
||||
| -- | Execute the 'Backup' command
|
||||
CBackup
|
||||
| -- | Execute the 'Path' command
|
||||
CPath
|
||||
| -- | Execute the 'Push' command
|
||||
CPush
|
||||
| -- | Execute the 'Stride' command
|
||||
@ -74,6 +76,8 @@ data Capability
|
||||
CSalvage
|
||||
| -- | Execute the 'Drill' command
|
||||
CDrill
|
||||
| -- | Execute the 'Waypoint' command
|
||||
CWaypoint
|
||||
| -- | Execute the 'Whereami' command
|
||||
CSenseloc
|
||||
| -- | Execute the 'Blocked' command
|
||||
@ -212,6 +216,7 @@ constCaps = \case
|
||||
Selfdestruct -> Just CSelfdestruct
|
||||
Move -> Just CMove
|
||||
Backup -> Just CBackup
|
||||
Path -> Just CPath
|
||||
Push -> Just CPush
|
||||
Stride -> Just CMovemultiple
|
||||
Turn -> Just CTurn
|
||||
@ -252,7 +257,7 @@ constCaps = \case
|
||||
Wait -> Just CTimerel
|
||||
Scout -> Just CRecondir
|
||||
Whereami -> Just CSenseloc
|
||||
Waypoint -> Just CGod
|
||||
Waypoint -> Just CWaypoint
|
||||
Detect -> Just CDetectloc
|
||||
Resonate -> Just CDetectcount
|
||||
Density -> Just CDetectcount
|
||||
|
@ -40,6 +40,7 @@ module Swarm.Language.Syntax (
|
||||
maxSniffRange,
|
||||
maxScoutRange,
|
||||
maxStrideRange,
|
||||
maxPathRange,
|
||||
|
||||
-- * Syntax
|
||||
Syntax' (..),
|
||||
@ -118,6 +119,9 @@ maxScoutRange = 64
|
||||
maxStrideRange :: Int
|
||||
maxStrideRange = 64
|
||||
|
||||
maxPathRange :: Integer
|
||||
maxPathRange = 128
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Constants
|
||||
------------------------------------------------------------
|
||||
@ -152,6 +156,8 @@ data Const
|
||||
Move
|
||||
| -- | Move backward one step.
|
||||
Backup
|
||||
| -- | Describe a path to the destination.
|
||||
Path
|
||||
| -- | Push an entity forward one step.
|
||||
Push
|
||||
| -- | Move forward multiple steps.
|
||||
@ -525,6 +531,12 @@ constInfo c = case c of
|
||||
]
|
||||
Move -> command 0 short "Move forward one step."
|
||||
Backup -> command 0 short "Move backward one step."
|
||||
Path ->
|
||||
command 2 short . doc "Obtain shortest path to the destination." $
|
||||
[ "Optionally supply a distance limit as the first argument."
|
||||
, "Supply either a location (`inL`) or an entity (`inR`) as the second argument."
|
||||
, "If a path exists, returns the direction to proceed along."
|
||||
]
|
||||
Push ->
|
||||
command 1 short . doc "Push an entity forward one step." $
|
||||
[ "Both entity and robot moves forward one step."
|
||||
|
@ -742,6 +742,7 @@ inferConst c = case c of
|
||||
Selfdestruct -> [tyQ| cmd unit |]
|
||||
Move -> [tyQ| cmd unit |]
|
||||
Backup -> [tyQ| cmd unit |]
|
||||
Path -> [tyQ| (unit + int) -> ((int * int) + entity) -> cmd (unit + dir) |]
|
||||
Push -> [tyQ| cmd unit |]
|
||||
Stride -> [tyQ| int -> cmd unit |]
|
||||
Turn -> [tyQ| dir -> cmd unit |]
|
||||
|
@ -27,6 +27,7 @@ import Swarm.TUI.Editor.Util qualified as EU
|
||||
import Swarm.TUI.Model
|
||||
import Swarm.TUI.Model.Name
|
||||
import Swarm.TUI.Model.UI
|
||||
import Swarm.Util (hoistMaybe)
|
||||
import Swarm.Util.Erasable (maybeToErasable)
|
||||
import System.Clock
|
||||
|
||||
@ -57,8 +58,7 @@ handleCtrlLeftClick mouseLoc = do
|
||||
let getSelected x = snd <$> BL.listSelectedElement x
|
||||
maybeTerrainType = getSelected $ worldEditor ^. terrainList
|
||||
maybeEntityPaint = getSelected $ worldEditor ^. entityPaintList
|
||||
-- TODO (#1151): Use hoistMaybe when available
|
||||
terrain <- MaybeT . pure $ maybeTerrainType
|
||||
terrain <- hoistMaybe maybeTerrainType
|
||||
mouseCoords <- MaybeT $ Brick.zoom gameState $ mouseLocToWorldCoords mouseLoc
|
||||
uiState . uiWorldEditor . worldOverdraw . paintedTerrain %= M.insert (mouseCoords ^. planar) (terrain, maybeToErasable maybeEntityPaint)
|
||||
uiState . uiWorldEditor . lastWorldEditorMessage .= Nothing
|
||||
|
@ -25,6 +25,7 @@ module Swarm.Util (
|
||||
allEqual,
|
||||
surfaceEmpty,
|
||||
applyWhen,
|
||||
hoistMaybe,
|
||||
|
||||
-- * Directory utilities
|
||||
readFileMay,
|
||||
@ -79,6 +80,7 @@ import Control.Carrier.Throw.Either
|
||||
import Control.Effect.State (State, modify, state)
|
||||
import Control.Lens (ASetter', Lens', LensLike, LensLike', Over, lens, (<&>), (<>~))
|
||||
import Control.Monad (filterM, guard, unless)
|
||||
import Control.Monad.Trans.Maybe (MaybeT (..))
|
||||
import Data.Bifunctor (Bifunctor (bimap), first)
|
||||
import Data.Char (isAlphaNum, toLower)
|
||||
import Data.Either.Validation
|
||||
@ -206,12 +208,21 @@ allEqual (x : xs) = all (== x) xs
|
||||
surfaceEmpty :: Alternative f => (a -> Bool) -> a -> f a
|
||||
surfaceEmpty isEmpty t = t <$ guard (not (isEmpty t))
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Forward-compatibility functions
|
||||
|
||||
-- Note, once we upgrade to an LTS version that includes
|
||||
-- base-compat-0.13, we should switch to using 'applyWhen' from there.
|
||||
applyWhen :: Bool -> (a -> a) -> a -> a
|
||||
applyWhen True f x = f x
|
||||
applyWhen False _ x = x
|
||||
|
||||
-- | Convert a 'Maybe' computation to 'MaybeT'.
|
||||
--
|
||||
-- TODO (#1151): Use implementation from "transformers" package v0.6.0.0
|
||||
hoistMaybe :: (Applicative m) => Maybe b -> MaybeT m b
|
||||
hoistMaybe = MaybeT . pure
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Directory stuff
|
||||
|
||||
|
@ -4,6 +4,7 @@ extra-deps:
|
||||
- simple-enumeration-0.2.1@sha256:8625b269c1650d3dd0e3887351c153049f4369853e0d525219e07480ea004b9f,1178
|
||||
- boolexpr-0.2@sha256:07f38a0206ad63c2c893e3c6271a2e45ea25ab4ef3a9e973edc746876f0ab9e8,853
|
||||
- brick-1.10
|
||||
- astar-0.3.0.0
|
||||
- brick-list-skip-0.1.1.5
|
||||
# We should update to lsp-2.0 and lsp-types-2.0 but it involves some
|
||||
# breaking changes; see https://github.com/swarm-game/swarm/issues/1350
|
||||
|
@ -147,6 +147,7 @@ library
|
||||
Swarm.Game.State
|
||||
Swarm.Game.Step
|
||||
Swarm.Game.Step.Combustion
|
||||
Swarm.Game.Step.Pathfinding
|
||||
Swarm.Game.Step.Util
|
||||
Swarm.Game.Terrain
|
||||
Swarm.Game.Value
|
||||
@ -231,6 +232,7 @@ library
|
||||
brick-list-skip >= 0.1.1.2 && < 0.2,
|
||||
aeson >= 2 && < 2.2,
|
||||
array >= 0.5.4 && < 0.6,
|
||||
astar >= 0.3 && < 0.3.1,
|
||||
blaze-html >= 0.9.1 && < 0.9.2,
|
||||
boolexpr >= 0.2 && < 0.3,
|
||||
brick >= 1.10 && < 1.11,
|
||||
|
@ -333,6 +333,7 @@ testScenarioSolutions rs ui =
|
||||
, testSolution Default "Testing/1355-combustion"
|
||||
, testSolution Default "Testing/1379-single-world-portal-reorientation"
|
||||
, testSolution Default "Testing/1399-backup-command"
|
||||
, testSolution Default "Testing/1536-custom-unwalkable-entities"
|
||||
, testGroup
|
||||
-- Note that the description of the classic world in
|
||||
-- data/worlds/classic.yaml (automatically tested to some
|
||||
@ -344,6 +345,17 @@ testScenarioSolutions rs ui =
|
||||
, testSolution Default "Testing/1320-world-DSL/erase"
|
||||
, testSolution Default "Testing/1320-world-DSL/override"
|
||||
]
|
||||
, testGroup
|
||||
"Pathfinding (#836)"
|
||||
[ testSolution Default "Testing/836-pathfinding/836-path-exists-find-entity"
|
||||
, testSolution Default "Testing/836-pathfinding/836-path-exists-find-location"
|
||||
, testSolution Default "Testing/836-pathfinding/836-path-exists-find-entity-unwalkable"
|
||||
, testSolution Default "Testing/836-pathfinding/836-path-exists-distance-limit-unreachable"
|
||||
, testSolution Default "Testing/836-pathfinding/836-path-exists-distance-limit-unreachable"
|
||||
, testSolution Default "Testing/836-pathfinding/836-no-path-exists1"
|
||||
, testSolution (Sec 10) "Testing/836-pathfinding/836-no-path-exists2"
|
||||
, testSolution (Sec 3) "Testing/836-pathfinding/836-automatic-waypoint-navigation.yaml"
|
||||
]
|
||||
]
|
||||
, testSolution' Default "Testing/1430-built-robot-ownership" CheckForBadErrors $ \g -> do
|
||||
let r2 = g ^. robotMap . at 2
|
||||
|
Loading…
Reference in New Issue
Block a user