Use a new opaque type for robots instead of strings (#303)

The basic idea of this change is to create a new `robot` type and use it to identify robots instead of `string` names.  Internally, a `robot` value is just a (unique) `Int`. 

Closes #212 .

This ended up turning into a sort of constellation of related changes.

- Add the `robot` type and change the type of various built-in functions which used to take a robot name so they now take a `robot` (`give`, `install`, `reprogram`, `view`, `upload`) and change `build` so it returns a `robot`.
- All internal data structures that store robots are now keyed by a unique (`Int`) robot ID rather than by name.
- Add a `setname` command for setting a robot's display name (which no longer needs to uniquely identify a robot).
- Import a big list of words which we can use to randomly pick names for robots, just for fun.  This is why the diff says "+31,050  -265"; I did not write 31 thousand lines of code.
- Add constants `base`, `parent`, and `self` for getting a `robot` value referring to the base, one's parent, and one's self, respectively.
- Top-level binders like `r <- build {move}` now export a variable binding which can be used in later expressions entered at the REPL; additionally, unlike Haskell, a binder can now appear as the last statement in a block.
- Fix the pretty-printer for `Value` by doubling down on our current strategy of injecting `Value`s back into `Term`s and then pretty-printing the result.  I am now convinced this is the Right Way (tm) to do this; it only required adding a couple additional kinds of `Term` which represent internal results of evaluation and cannot show up in the surface language (`TRef`, `TRobot`).
- Update the tutorial.
- While updating the tutorial, I noticed that #294 had introduced a bug, where the inventory display no longer updated when 0 copies of an entity are added to the inventory (as with `scan` + `upload`), so I fixed that by changing the way inventory hashes are computed.

I tried running the benchmarks both before & after this change.  I was hoping that it might speed things up to be using `IntMap` and `IntSet` instead of looking things up by `Text` keys in a `Map` all the time.  However, if I'm interpreting the results correctly, it seems like it didn't really make all that much difference, at least for the particular benchmarks we have.
This commit is contained in:
Brent Yorgey 2022-03-01 21:00:44 -06:00 committed by GitHub
parent 47e1e7d074
commit b62d27e566
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 912 additions and 350 deletions

6
NOTICE Normal file
View File

@ -0,0 +1,6 @@
Swarm
Copyright 2022 Swarm contributors
This product includes data from the "namesgenerator" package, found at
https://github.com/moby/moby/blob/master/pkg/namesgenerator/names-generator.go
and used under the terms of the Apache 2.0 License, https://www.apache.org/licenses/

View File

@ -64,21 +64,23 @@ Pretty much the only thing you can do is build robots. Let's build
one! Tab back to the REPL (or hit the <kbd>Meta</kbd>+<kbd>R</kbd>
shortcut) and type
```
build "hello" {turn north; move}
build {turn north; move}
```
then hit Enter. You should see a robot appear and travel to the
north one step before stopping. It should look something like this:
![Hello robot!](images/tutorial/hello.png)
You can also see that on the next line after your input, the REPL printed out
You can also see that on the next line after your input, the REPL
printed out something like
```
"hello" : string
<r1> : robot
```
which is the result of your command, along with its type. The `build` command
always returns a string which is the name of the robot that was built;
it may be different than the name you specified if there is already
another robot with that name.
which is the result of your command, along with its type. The `build`
command always returns a reference to the robot that it built. Note
that `<r1>` is not special syntax, just an opaque name for the robot
with ID 1.
You can see that a semicolon is used to chain together commands, that
is, if `c1` and `c2` are both commands, then `c1; c2` is the command
@ -93,22 +95,22 @@ Types
-----
Ultimately, the `build` command is not special syntax, but just a
function that takes two arguments: a string, and a delayed command. We
can actually see the type of the `build` command (or any command) by
just typing it at the prompt, without hitting `Enter`. Any time the
function that takes an argument representing a delayed command. We
can actually see the type of `build` (or anything else) by
typing it at the prompt, without hitting `Enter`. Any time the
expression currently typed at the prompt parses and type checks, the
REPL will show you the type of the expression in the upper right, like
this:
![Build type](images/tutorial/build.png)
It will tell you that the type of `build` is
In this case, we can see that the type of `build` is
```
∀ a0. string -> {cmd a0} -> cmd string
∀ a0. {cmd a0} -> cmd robot
```
which says that `build` takes two arguments---a `string`, and a
which says that `build` takes one argument---a
delayed command that returns a value of any type---and results in a command
which returns a `string`. Every command returns a value, though some
which returns a `robot`. Every command returns a value, though some
might return a value of the unit type, written `()`. For example, if
you type `move` at the prompt, you will see that its type is `cmd ()`,
since `move` does not return any interesting result after executing.
@ -134,7 +136,7 @@ Something you can't do yet
Try entering the following at the REPL:
```
build "nope" {make "curry"}
build {make "curry"}
```
The info panel should automatically switch to showing your `logger`
device, with an error message at the bottom saying something like
@ -160,11 +162,11 @@ our life a bit easier. To start, type the following:
def m : cmd () = move end
```
The `: cmd ()` annotation on `m` is optional; in this situation the
game could have easily figured out the type of `m` if we had just
written `def m = ...` (though there are some situations where a type
signature may be required). The `end` is required, and is needed to
disambiguate where the end of the definition is.
The `: cmd ()` annotation on `m` is optional; in this case the game
can easily figure out the type of `m` if we just write `def m = move end`
(though there are some situations where a type signature may be
required). The `end` is required, and is needed to disambiguate where
the end of the definition is.
Now try this:
```
@ -178,14 +180,15 @@ def m2 = m; m end; def m4 = m2; m2 end; def m8 = m4; m4 end
Great, now we have commands that will execute `move` multiple times.
Now let's use them:
```
build "runner" { turn west; m4; m }
build { turn west; m4; m }
```
This should build a robot that moves to the green mass to the west.
This should build a robot that moves toward the green mass to the west.
You might wonder at this point if it is possible to create a function
that takes a number as input and moves that many steps forward. It
most certainly is, but right now your robots would not be capable
of executing it. You'll have to figure out how to upgrade them!
that takes a number as input and moves that many steps forward, like
`go : int -> cmd ()`. Well, it most certainly is possible, but right
now your robots would not be capable of executing it. You'll have to
figure out how to upgrade them!
Getting the result of a command
-------------------------------
@ -199,12 +202,11 @@ var <- command; ... more commands that can refer to var ...
monad, similar to the `IO` in Haskell. But if that doesn't mean
anything to you, don't worry about it!</sup>
Let's build one more robot called `"runner"`. It will get renamed
to something else to avoid a name conflict, but we can capture its
name in a variable using the above syntax.
Then we can use the `view` command to focus on it instead of the base:
Let's build another robot, but this time we will capture it in a
variable using the above syntax. Then we can use the `view` command
to focus on it instead of the base:
```
r <- build "runner" { turn west; m4; m }; view r
r <- build { turn west; m4; m }; view r
```
Note that `base` executes the `view r` command as soon as it
finishes executing the `build` command, which is about the same time
@ -214,39 +216,50 @@ view should look something like this:
![View a robot](images/tutorial/viewr.png)
The view is now centered on `runner1` instead of on our `base`, and the
top-left panel shows `runner1`'s inventory and installed devices
The view is now centered on the other robot instead of on our `base`,
and the top-left panel shows its inventory and installed devices
instead of `base`'s. (However, commands entered at the REPL will
still be executed by `base`.) To return to viewing `base` and its
inventory, you can type `view "base"` at the prompt, or focus the
world panel (either using <kbd>Tab</kbd> or <kbd>Meta</kbd>+<kbd>W</kbd>)
and hit <kbd>C</kbd>.
still be executed by `base`.) The name of your robot will likely be
different than `sleepy_austin`; otherwise unnamed robots are given
randomly chosen names. To return to viewing `base` and its inventory,
you can type `view base` at the prompt, or focus the world panel
(either using <kbd>Tab</kbd> or <kbd>Meta</kbd>+<kbd>W</kbd>) and hit
<kbd>C</kbd>. If you ever want to view the other robot again, you can
type `view r`: the variable `r` will be in scope at the REPL prompt
from now on.
You can set the display name of a robot using the `setname` command.
The randomly generated names are amusing, but being able to set the
display name explicitly can help with debugging.
Exploring
---------
So what is all this stuff everywhere? Let's find out! When you
`build` a robot, by default it starts out with a `scanner` device,
which you may have noticed in `runner1`'s inventory. You can `scan`
items in the world to learn about them, and later `upload` what you
have learned to the base.
which you may have noticed in the recently viewed robot's inventory.
You can `scan` items in the world to learn about them, and later
`upload` what you have learned to the base.
Let's build a robot to learn about those green `?` things to the west:
```
build "s" {turn west; m4; move; scan west; turn back; m4; upload "base"}
build {turn west; m4; move; scan west; turn back; m4; upload base}
```
The `turn` command we used to turn the robot takes a direction as an
argument, which can be either an absolute direction
(`north`, `south`, `east`, or `west`) or a relative direction
(`forward`, `back`, `left`, `right`, or `down`).
(`forward`, `back`, `left`, `right`, or `down`). Instead of `upload
base` we could have also written `upload parent`; every robot has a
special variable `parent` which refers to the robot that built it.
Notice that the robot did not actually need to walk on top of a `?` to
learn about it, since it could `scan west` to scan the cell one unit
to the west (you can also `scan down` to scan an item directly underneath the
to the west (you can also `scan down` to scan an item directly beneath the
robot). Also, it was able to `upload` at a distance of one cell away from
the base.
After this robot finishes, you should have a new entry in your inventory:
After this robot finishes, you should have a new entry in your
inventory:
![Scan a tree](images/tutorial/scantree.png)
@ -261,7 +274,7 @@ Getting some resources
So those tree things look like they might be useful. Let's get one!
```
build "fetch" {turn west; m8; thing <- grab; turn back; m8; give "base" thing }
build {turn west; m8; thing <- grab; turn back; m8; give base thing }
```
You can see that the
`grab` command returns the name of the thing it grabbed, which is
@ -325,19 +338,22 @@ any devices that will be necessary to execute it. (It is also
possible to manually install devices with the `install` command.) So
let's type the following:
```
build "crasher" {log "hi!"; turn south; move; grab; move}
crasher <- build {setname "crasher"; log "hi!"; turn south; move; grab; move}
```
The world should now look something like the below. Notice that the
(The `setname "crasher"` command is not strictly necessary, but will
help us understand the logs we look at later --- otherwise the log
entries would be indexed by some randomly generated robot name.) The
world should now look something like the below. Notice that the
`logger` is gone from your inventory---it was automatically installed
on `crasher`. Notice also that `crasher` only moved one unit south,
even though we told it to move two steps! What went wrong?
![Let's crash a robot!](images/tutorial/crasher.png)
One thing we could do at this point is to `view "crasher"`. However,
One thing we could do at this point is to `view crasher`. However,
it will probably become a bit more difficult to use the `view` command in
future versions of the game, and in any case, what if we didn't know
or remember the name of the robot that crashed? Fortunately, there is
future versions of the game, and in any case, what if we didn't have
a reference to the robot that crashed? Fortunately, there is
something else we can do: send out another robot to `salvage` the
crashed robot.
@ -359,10 +375,9 @@ the `upload` command, which we have seen before. In addition to
uploading knowledge about entities, it turns out that it also uploads
the log from a `logger`.
```
build "fetch" {turn west; m8; m; thing <- grab; turn back; m8; m; give "base" thing}
make "log"
make "logger"
build "salvager" {turn south; move; log "salvaging..."; salvage; turn back; move; upload "base"}
build {turn west; m8; m; thing <- grab; turn back; m8; m; give base thing}
make "log"; make "logger"
build {setname "salvager"; turn south; move; log "salvaging..."; salvage; turn back; move; upload base}
```
The world should now look something like this:

View File

@ -12,6 +12,7 @@ import Criterion.Types (Config (timeLimit))
import Data.Int (Int64)
import Linear.V2 (V2 (V2))
import Swarm.Game.CESK (emptyStore, initMachine)
import Swarm.Game.Display (defaultRobotDisplay)
import Swarm.Game.Robot (Robot, mkRobot)
import Swarm.Game.State (GameState, GameType (ClassicGame), addRobot, creativeMode, initGameState, world)
import Swarm.Game.Step (gameTick)
@ -70,7 +71,7 @@ circlerProgram =
-- | Initializes a robot with program prog at location loc facing north.
initRobot :: ProcessedTerm -> V2 Int64 -> Robot
initRobot prog loc = mkRobot "" north loc (initMachine prog Context.empty emptyStore) []
initRobot prog loc = mkRobot (-1) Nothing "" [] north loc defaultRobotDisplay (initMachine prog Context.empty emptyStore) [] [] False
-- | Creates a GameState with numRobot copies of robot on a blank map, aligned
-- in a row starting at (0,0) and spreading east.

109
data/adjectives.txt Normal file
View File

@ -0,0 +1,109 @@
# Adapted from https://github.com/moby/moby/blob/master/pkg/namesgenerator/names-generator.go
admiring
adoring
affectionate
agitated
amazing
angry
awesome
beautiful
blissful
bold
boring
brave
busy
charming
clever
cool
compassionate
competent
condescending
confident
cranky
crazy
dazzling
determined
distracted
dreamy
eager
ecstatic
elastic
elated
elegant
eloquent
epic
exciting
fervent
festive
flamboyant
focused
friendly
frosty
funny
gallant
gifted
goofy
gracious
great
happy
hardcore
heuristic
hopeful
hungry
infallible
inspiring
interesting
intelligent
jolly
jovial
keen
kind
laughing
loving
lucid
magical
mystifying
modest
musing
naughty
nervous
nice
nifty
nostalgic
objective
optimistic
peaceful
pedantic
pensive
practical
priceless
quirky
quizzical
recursing
relaxed
reverent
romantic
sad
serene
sharp
silly
sleepy
stoic
strange
stupefied
suspicious
sweet
tender
thirsty
trusting
unruffled
upbeat
vibrant
vigilant
vigorous
wizardly
wonderful
xenodochial
youthful
zealous
zen

View File

@ -13,7 +13,7 @@ entities:
properties: [portable]
win: |
try {
loc <- as "base" {whereami};
loc <- as base {whereami};
return (loc == (2,0))
} { return false }
robots:

View File

@ -21,7 +21,7 @@ entities:
win: |
try {
loc <- as "base" {whereami};
loc <- as base {whereami};
return (loc == (3,1))
} { return false }
robots:

View File

@ -13,7 +13,7 @@ entities:
properties: [portable]
win: |
try {
loc <- as "base" {whereami};
loc <- as base {whereami};
return (loc == (2,0))
} { return false }
robots:

View File

@ -365,10 +365,10 @@
place. The item is removed from the robot's inventory and placed
in the robot's current cell (which must be empty). Raises an
exception if the operation fails.
- "The 'give' command takes two arguments: the name of the robot to
- "The 'give' command takes two arguments: the robot to
give an item to (which can be at most 1 cell away), and the name
of the item to give. Raises an exception if the operation fails."
- "The 'install' command takes two arguments: the name of the robot
- "The 'install' command takes two arguments: the robot
on which to install a device (which can be at most 1 cell away),
and the name of the device to install."
@ -418,17 +418,14 @@
- A 3D printer gives you the capability of printing more robots! You
can access the 3D printer via the 'build' command.
- 'Example:'
- ' build "fred" {move; grab; turn back; move; give "base"
"tree"}'
- ' build {move; grab; turn back; move; give base "tree"}'
- |
builds a robot named "fred" to get the tree on the cell to the
east (if there is one) and bring it back to you. Note that if
there is already a robot named "fred", the new robot will be given
a similar but unique name. The 'build' command always returns the
name of the newly constructed robot. For example,
- ' name <- build "fred" {move}; view name'
builds a robot to get the tree on the cell to the
east (if there is one) and bring it back to the base. The 'build' command
always returns a reference to the newly constructed robot. For example,
- ' r <- build {move}; view r'
- |
builds a robot and then views it (no matter what it ends up being named).
builds a robot and then views it.
properties: [portable]
capabilities: [build]
@ -461,9 +458,9 @@
A branch predictor is a device which allows a robot to interpret
conditional expressions. The syntax for a conditional expression
is 'if' followed by three arguments: a boolean test and then
two expressions of the same type.
two delayed expressions of the same type.
- 'Example:'
- 'if (x > 3) move {turn west; move}'
- 'if (x > 3) {move} {turn west; move}'
properties: [portable]
capabilities: [cond]
@ -506,7 +503,7 @@
boolean value indicating whether the robot's path is blocked
(i.e. whether executing a 'move' command would fail)."
- "Finally, robots can use the 'upload' command to copy their accumulated
knowledge to another nearby robot; for example, 'upload \"base\"'."
knowledge to another nearby robot; for example, 'upload base'."
properties: [portable]
capabilities: [scan, sensefront, sensehere]
@ -519,8 +516,8 @@
'reprogram' command."
- "Only robots that have completed executing their previous command
can be reprogrammed."
- "The robot must possess enough capabilities to run the new command
otherwise it will fail."
- "The robot being reprogrammed must possess enough capabilities to run the new command;
otherwise reprogramming will fail."
properties: [portable]
capabilities: [reprogram]

238
data/names.txt Normal file
View File

@ -0,0 +1,238 @@
# Adapted from https://github.com/moby/moby/blob/master/pkg/namesgenerator/names-generator.go
agnesi
albattani
allen
almeida
antonelli
archimedes
ardinghelli
aryabhata
austin
babbage
banach
banzai
bardeen
bartik
bassi
beaver
bell
benz
bhabha
bhaskara
black
blackburn
blackwell
bohr
booth
borg
bose
bouman
boyd
brahmagupta
brattain
brown
buck
burnell
cannon
carson
cartwright
carver
cerf
chandrasekhar
chaplygin
chatelet
chatterjee
chaum
chebyshev
clarke
cohen
colden
cori
cray
curran
curie
darwin
davinci
dewdney
dhawan
diffie
dijkstra
dirac
driscoll
dubinsky
easley
edison
einstein
elbakyan
elgamal
elion
ellis
engelbart
euclid
euler
faraday
feistel
fermat
fermi
feynman
franklin
gagarin
galileo
galois
ganguly
gates
gauss
germain
goldberg
goldstine
goldwasser
golick
goodall
gould
greider
grothendieck
haibt
hamilton
haslett
hawking
hellman
heisenberg
hermann
herschel
hertz
heyrovsky
hodgkin
hofstadter
hoover
hopper
hugle
hypatia
ishizaka
jackson
jang
jemison
jennings
jepsen
johnson
joliot
jones
kalam
kapitsa
kare
keldysh
keller
kepler
khayyam
khorana
kilby
kirch
knuth
kowalevski
lalande
lamarr
lamport
leakey
leavitt
lederberg
lehmann
lewin
lichterman
liskov
lovelace
lumiere
mahavira
margulis
matsumoto
maxwell
mayer
mccarthy
mcclintock
mclaren
mclean
mcnulty
mendel
mendeleev
meitner
meninsky
merkle
mestorf
mirzakhani
montalcini
moore
morse
murdock
moser
napier
nash
neumann
newton
nightingale
nobel
noether
northcutt
noyce
panini
pare
pascal
pasteur
payne
perlman
pike
poincare
poitras
proskuriakova
ptolemy
raman
ramanujan
ride
ritchie
rhodes
robinson
roentgen
rosalind
rubin
saha
sammet
sanderson
satoshi
shamir
shannon
shaw
shirley
shockley
shtern
sinoussi
snyder
solomon
spence
stonebraker
sutherland
swanson
swartz
swirles
taussig
tereshkova
tesla
tharp
thompson
torvalds
tu
turing
varahamihira
vaughan
villani
visvesvaraya
volhard
wescoff
wilbur
wiles
williams
williamson
wilson
wing
wozniak
wright
wu
yalow
yonath
zhukovsky

View File

@ -43,7 +43,7 @@ def spawnfwd : {cmd ()} -> cmd () = \c.
move;
b <- isHere "tree";
if b
{ build "s" c; return () }
{ build c; return () }
{};
turn back;
move
@ -56,7 +56,7 @@ def clear : cmd () =
turn left
);
goto 0 0;
give "base" "tree";
give base "tree";
selfdestruct;
end;
def start : cmd string = build "h" {turn west; repeat 7 move; clear} end
def start : cmd robot = build {turn west; repeat 7 move; clear} end

View File

@ -7,6 +7,6 @@ in
rep 4 (
rep 10 move;
turn left;
build "sq" {run("square.sw")};
build {run("square.sw")};
return ()
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -472,7 +472,12 @@ data Inventory = Inventory
, -- Mirrors the main map; just caching the ability to look up by
-- name.
byName :: Map Text IntSet
, -- Cached hash of the inventory.
, -- Cached hash of the inventory, using a homomorphic hashing scheme
-- (see https://github.com/byorgey/swarm/issues/229).
--
-- Invariant: equal to Sum_{(k,e) \in counts} (k+1) * (e ^. entityHash).
-- The k+1 is so the hash distinguishes between having a 0 count of something
-- and not having it as a key in the map at all.
inventoryHash :: Int
}
deriving (Show, Generic)
@ -533,7 +538,17 @@ insertCount k e (Inventory cs byN h) =
Inventory
(IM.insertWith (\(m, _) (n, _) -> (m + n, e)) (e ^. entityHash) (k, e) cs)
(M.insertWith IS.union (T.toLower $ e ^. entityName) (IS.singleton (e ^. entityHash)) byN)
(h + k * (e ^. entityHash)) -- homomorphic hashing
(h + (k + extra) * (e ^. entityHash)) -- homomorphic hashing
where
-- Include the hash of an entity once just for "knowing about" it;
-- then include the hash once per actual copy of the entity. In
-- other words, having k copies of e in the inventory contributes
-- (k+1)*(e ^. entityHash) to the inventory hash. The reason for
-- doing this is so that the inventory hash changes even when we
-- insert 0 copies of something, since having 0 copies of something
-- is different than not having it as a key at all; having 0 copies
-- signals that we at least "know about" the entity.
extra = if (e ^. entityHash) `IM.member` cs then 0 else 1
-- | Check whether an inventory contains at least one of a given entity.
contains :: Inventory -> Entity -> Bool
@ -578,4 +593,10 @@ union (Inventory cs1 byN1 h1) (Inventory cs2 byN2 h2) =
Inventory
(IM.unionWith (\(c1, e) (c2, _) -> (c1 + c2, e)) cs1 cs2)
(M.unionWith IS.union byN1 byN2)
(h1 + h2)
(h1 + h2 - common)
where
-- Need to subtract off the sum of the hashes in common, because
-- of the way each entity with count k contributes (k+1) times its
-- hash. So if the two inventories share an entity e, just adding their
-- hashes would mean e now contributes (k+2) times its hash.
common = IS.foldl' (+) 0 $ (IM.keysSet cs1) `IS.intersection` (IM.keysSet cs2)

View File

@ -20,6 +20,7 @@ module Swarm.Game.Robot (
leTime,
-- * Robots
RID,
Robot,
-- * Robot context
@ -42,6 +43,8 @@ module Swarm.Game.Robot (
inventoryHash,
robotCapabilities,
robotContext,
robotID,
robotParentID,
machine,
systemRobot,
selfDestruct,
@ -49,8 +52,8 @@ module Swarm.Game.Robot (
-- ** Create
mkRobot,
mkRobot',
baseRobot,
unsafeSetRobotID,
-- ** Query
robotKnows,
@ -115,6 +118,9 @@ data LogEntry = LogEntry
makeLenses ''LogEntry
-- | A unique identifier for a robot.
type RID = Int
-- | A value of type 'Robot' is a record representing the state of a
-- single robot.
data Robot = Robot
@ -127,6 +133,8 @@ data Robot = Robot
, _robotLogUpdated :: Bool
, _robotLocation :: V2 Int64
, _robotContext :: RobotContext
, _robotID :: RID
, _robotParentID :: Maybe RID
, _machine :: CESK
, _systemRobot :: Bool
, _selfDestruct :: Bool
@ -137,7 +145,7 @@ data Robot = Robot
-- See https://byorgey.wordpress.com/2021/09/17/automatically-updated-cached-views-with-lens/
-- for the approach used here with lenses.
let exclude = ['_robotCapabilities, '_installedDevices, '_robotLog]
let exclude = ['_robotCapabilities, '_installedDevices, '_robotLog, '_robotID]
in makeLensesWith
( lensRules
& generateSignatures .~ False
@ -181,6 +189,28 @@ robotInventory = robotEntity . entityInventory
-- | The robot's context
robotContext :: Lens' Robot RobotContext
-- | The (unique) ID number of the robot. This is only a Getter since
-- the robot ID is immutable.
robotID :: Getter Robot RID
robotID = to _robotID
-- | Set the ID number of a robot. This is "unsafe" since robots
-- should be uniquely identified by their ID, and are stored using
-- the ID as a key, etc. The only place this is ever needed is when
-- reading robots from a `.yaml` file (*e.g.* in a challenge
-- description), we cannot fill in a unique ID at parse time since
-- we don't have access to a `State Game` effect; when later adding
-- such robots to the world we generate and fill in a unique ID.
-- Otherwise, all robots are created via 'mkRobot', which requires
-- an ID number up front.
unsafeSetRobotID :: RID -> Robot -> Robot
unsafeSetRobotID i r = r {_robotID = i}
-- | The ID number of the robot's parent, that is, the robot that
-- built (or most recently reprogrammed) this robot, if there is
-- one.
robotParentID :: Lens' Robot (Maybe RID)
-- | A separate inventory for "installed devices", which provide the
-- robot with certain capabilities.
--
@ -293,45 +323,12 @@ selfDestruct :: Lens' Robot Bool
-- can tell when the counter increments.
tickSteps :: Lens' Robot Int
-- | Create a robot.
-- | A general function for creating robots.
mkRobot ::
-- | Name of the robot. Precondition: it should not be the same as any
-- other robot name.
Text ->
-- | Initial location.
V2 Int64 ->
-- | Initial heading/direction.
V2 Int64 ->
-- | Initial CESK machine.
CESK ->
-- | Installed devices.
[Entity] ->
Robot
mkRobot name l d m devs =
Robot
{ _robotEntity =
mkEntity
defaultRobotDisplay
name
["A generic robot."]
[]
& entityOrientation ?~ d
, _installedDevices = inst
, _robotCapabilities = inventoryCapabilities inst
, _robotLog = Seq.empty
, _robotLogUpdated = False
, _robotLocation = l
, _robotContext = RobotContext empty empty empty emptyStore
, _machine = m
, _systemRobot = False
, _selfDestruct = False
, _tickSteps = 0
}
where
inst = fromList devs
-- | A more general function for creating robots.
mkRobot' ::
-- | ID number of the robot.
Int ->
-- | ID number of the robot's parent, if it has one.
Maybe Int ->
-- | Name of the robot.
Text ->
-- | Description of the robot.
@ -351,7 +348,7 @@ mkRobot' ::
-- | Should this be a system robot?
Bool ->
Robot
mkRobot' name descr loc dir disp m devs inv sys =
mkRobot rid pid name descr loc dir disp m devs inv sys =
Robot
{ _robotEntity =
mkEntity disp name descr []
@ -363,6 +360,8 @@ mkRobot' name descr loc dir disp m devs inv sys =
, _robotLogUpdated = False
, _robotLocation = loc
, _robotContext = RobotContext empty empty empty emptyStore
, _robotID = rid
, _robotParentID = pid
, _machine = m
, _systemRobot = sys
, _selfDestruct = False
@ -389,6 +388,8 @@ baseRobot devs =
, _robotLogUpdated = False
, _robotLocation = V2 0 0
, _robotContext = RobotContext empty empty empty emptyStore
, _robotID = 0
, _robotParentID = Nothing
, _machine = idleMachine
, _systemRobot = False
, _selfDestruct = False
@ -401,7 +402,10 @@ baseRobot devs =
-- 'EntityMap' in which we can look up the names of entities.
instance FromJSONE EntityMap Robot where
parseJSONE = withObjectE "robot" $ \v ->
mkRobot'
-- Note we can't generate a unique ID here since we don't have
-- access to a 'State GameState' effect; a unique ID will be
-- filled in later when adding the robot to the world.
mkRobot (-1) Nothing
<$> liftE (v .: "name")
<*> liftE (v .:? "description" .!= [])
<*> liftE (v .: "loc")

View File

@ -36,6 +36,8 @@ module Swarm.Game.State (
activeRobots,
gensym,
randGen,
adjList,
nameList,
entityMap,
recipesOut,
recipesIn,
@ -46,7 +48,7 @@ module Swarm.Game.State (
replStatus,
replWorking,
messageQueue,
focusedRobotName,
focusedRobotID,
ticks,
-- * Utilities
@ -56,7 +58,6 @@ module Swarm.Game.State (
viewingRegion,
focusedRobot,
clearFocusedRobotLogUpdated,
ensureUniqueName,
addRobot,
emitMessage,
sleepUntil,
@ -69,25 +70,28 @@ module Swarm.Game.State (
import Control.Arrow (Arrow ((&&&)))
import Control.Lens hiding (use, uses, view, (%=), (+=), (.=), (<+=), (<<.=))
import Control.Monad.Except
import Data.Array (Array, listArray)
import Data.Bifunctor (first)
import Data.Int (Int64)
import Data.IntMap (IntMap)
import qualified Data.IntMap as IM
import Data.IntSet (IntSet)
import qualified Data.IntSet as IS
import Data.IntSet.Lens (setOf)
import Data.Map (Map)
import qualified Data.Map as M
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Set (Set)
import qualified Data.Set as S
import Data.Set.Lens (setOf)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text as T (lines)
import qualified Data.Text.IO as T (readFile)
import Linear
import System.Random (StdGen, mkStdGen)
import Witch (into)
import Control.Algebra (Has)
import Control.Effect.Lens
import Control.Effect.State (State)
import Paths_swarm (getDataFileName)
import Swarm.Game.Challenge
import Swarm.Game.Entity
import Swarm.Game.Recipe
@ -105,7 +109,7 @@ data ViewCenterRule
= -- | The view should be centered on an absolute position.
VCLocation (V2 Int64)
| -- | The view should be centered on a certain robot.
VCRobot Text
VCRobot RID
deriving (Eq, Ord, Show)
makePrisms ''ViewCenterRule
@ -152,12 +156,12 @@ data GameState = GameState
{ _creativeMode :: Bool
, _winCondition :: WinCondition
, _runStatus :: RunStatus
, _robotMap :: Map Text Robot
, _robotMap :: IntMap Robot
, -- A set of robots to consider for the next game tick. It is guaranteed to
-- be a subset of the keys of robotMap. It may contain waiting or idle
-- robots. But robots that are present in robotMap and not in activeRobots
-- are guaranteed to be either waiting or idle.
_activeRobots :: Set Text
_activeRobots :: IntSet
, -- A set of probably waiting robots, indexed by probable wake-up time. It
-- may contain robots that are in fact active or idle, as well as robots
-- that do not exist anymore. Its only guarantee is that once a robot name
@ -166,10 +170,12 @@ data GameState = GameState
-- wakeUpRobotsDoneSleeping.
-- Waiting robots for a given time are a list because it is cheaper to
-- append to a list than to a Set.
_waitingRobots :: Map Integer [Text]
, _robotsByLocation :: Map (V2 Int64) (Set Text)
_waitingRobots :: Map Integer [RID]
, _robotsByLocation :: Map (V2 Int64) IntSet
, _gensym :: Int
, _randGen :: StdGen
, _adjList :: Array Int Text
, _nameList :: Array Int Text
, _entityMap :: EntityMap
, _recipesOut :: IntMap [Recipe Entity]
, _recipesIn :: IntMap [Recipe Entity]
@ -179,7 +185,7 @@ data GameState = GameState
, _needsRedraw :: Bool
, _replStatus :: REPLStatus
, _messageQueue :: [Text]
, _focusedRobotName :: Text
, _focusedRobotID :: RID
, _ticks :: Integer
}
@ -187,7 +193,7 @@ data GameState = GameState
-- it as a Getter externally to protect invariants.
makeLensesFor [("_activeRobots", "internalActiveRobots")] ''GameState
let exclude = ['_viewCenter, '_focusedRobotName, '_viewCenterRule, '_activeRobots]
let exclude = ['_viewCenter, '_focusedRobotID, '_viewCenterRule, '_activeRobots, '_adjList, '_nameList]
in makeLensesWith
( lensRules
& generateSignatures .~ False
@ -211,7 +217,7 @@ paused :: Getter GameState Bool
paused = to (\s -> s ^. runStatus /= Running)
-- | All the robots that currently exist in the game, indexed by name.
robotMap :: Lens' GameState (Map Text Robot)
robotMap :: Lens' GameState (IntMap Robot)
-- | The names of all robots that currently exist in the game, indexed by
-- location (which we need both for /e.g./ the 'Salvage' command as
@ -222,15 +228,15 @@ robotMap :: Lens' GameState (Map Text Robot)
-- location of a robot changes, or a robot is created or destroyed.
-- Fortunately, there are relatively few ways for these things to
-- happen.
robotsByLocation :: Lens' GameState (Map (V2 Int64) (Set Text))
robotsByLocation :: Lens' GameState (Map (V2 Int64) IntSet)
-- | The names of the robots that are currently not sleeping.
activeRobots :: Getter GameState (Set Text)
activeRobots :: Getter GameState IntSet
activeRobots = internalActiveRobots
-- | The names of the robots that are currently sleeping, indexed by wake up
-- | time. Internal.
waitingRobots :: Lens' GameState (Map Integer [Text])
waitingRobots :: Lens' GameState (Map Integer [RID])
-- | A counter used to generate globally unique IDs.
gensym :: Lens' GameState Int
@ -238,6 +244,14 @@ gensym :: Lens' GameState Int
-- | Pseudorandom generator initialized at start.
randGen :: Lens' GameState StdGen
-- | Read-only list of words, for use in building random robot names.
adjList :: Getter GameState (Array Int Text)
adjList = to _adjList
-- | Read-only list of words, for use in building random robot names.
nameList :: Getter GameState (Array Int Text)
nameList = to _nameList
-- | The catalog of all entities that the game knows about.
entityMap :: Lens' GameState EntityMap
@ -266,11 +280,11 @@ viewCenterRule = lens getter setter
setter g rule =
case rule of
VCLocation v2 -> g {_viewCenterRule = rule, _viewCenter = v2}
VCRobot txt ->
let robotcenter = g ^? robotMap . ix txt <&> view robotLocation -- retrive the loc of the robot if it exist, Nothing otherwise. sometimes, lenses are amazing...
VCRobot rid ->
let robotcenter = g ^? robotMap . ix rid <&> view robotLocation -- retrive the loc of the robot if it exist, Nothing otherwise. sometimes, lenses are amazing...
in case robotcenter of
Nothing -> g
Just v2 -> g {_viewCenterRule = rule, _viewCenter = v2, _focusedRobotName = txt}
Just v2 -> g {_viewCenterRule = rule, _viewCenter = v2, _focusedRobotID = rid}
-- | The current center of the world view. Note that this cannot be
-- modified directly, since it is calculated automatically from the
@ -297,14 +311,14 @@ messageQueue :: Lens' GameState [Text]
-- | The current robot in focus. It is only a Getter because
-- this value should be updated only when viewCenterRule is.
focusedRobotName :: Getter GameState Text
focusedRobotName = to _focusedRobotName
focusedRobotID :: Getter GameState RID
focusedRobotID = to _focusedRobotID
-- | Given a current mapping from robot names to robots, apply a
-- 'ViewCenterRule' to derive the location it refers to. The result
-- is @Maybe@ because the rule may refer to a robot which does not
-- exist.
applyViewCenterRule :: ViewCenterRule -> Map Text Robot -> Maybe (V2 Int64)
applyViewCenterRule :: ViewCenterRule -> IntMap Robot -> Maybe (V2 Int64)
applyViewCenterRule (VCLocation l) _ = Just l
applyViewCenterRule (VCRobot name) m = m ^? at name . _Just . robotLocation
@ -346,45 +360,31 @@ viewingRegion g (w, h) = (W.Coords (rmin, cmin), W.Coords (rmax, cmax))
-- | Find out which robot is currently specified by the
-- 'viewCenterRule', if any.
focusedRobot :: GameState -> Maybe Robot
focusedRobot g = g ^? robotMap . ix (g ^. focusedRobotName)
focusedRobot g = g ^? robotMap . ix (g ^. focusedRobotID)
-- | Clear the 'robotLogUpdated' flag of the focused robot.
clearFocusedRobotLogUpdated :: Has (State GameState) sig m => m ()
clearFocusedRobotLogUpdated = do
n <- use focusedRobotName
n <- use focusedRobotID
robotMap . ix n . robotLogUpdated .= False
-- | Given a 'Robot', possibly modify its name to ensure that the name
-- is unique among robots. This is done simply by appending a new unique
ensureUniqueName :: Has (State GameState) sig m => Robot -> m Robot
ensureUniqueName newRobot = do
let name = newRobot ^. robotName
newName <- uniquifyRobotName name Nothing
return $ newRobot & robotName .~ newName
-- | Given a robot name, possibly add a numeric suffix to the end to
-- ensure it is unique.
uniquifyRobotName :: Has (State GameState) sig m => Text -> Maybe Int -> m Text
uniquifyRobotName name tag = do
let name' = name `T.append` maybe "" (into @Text . show) tag
collision <- uses robotMap (M.member name')
case collision of
True -> do
tag' <- gensym <+= 1
uniquifyRobotName name (Just tag')
False -> return name'
-- | Add a robot to the game state, possibly updating its name to
-- ensure it is unique, also adding it to the index of robots by
-- location, and return the (possibly modified) robot.
addRobot :: Has (State GameState) sig m => Robot -> m Robot
-- | Add a robot to the game state, adding it to the main robot map,
-- the active robot set, and to to the index of robots by
-- location. If it doesn't already have a unique ID number, generate
-- one for it.
addRobot :: Has (State GameState) sig m => Robot -> m ()
addRobot r = do
r' <- ensureUniqueName r
robotMap %= M.insert (r' ^. robotName) r'
r' <- case r ^. robotID of
(-1) -> do
rid <- gensym <+= 1
return (unsafeSetRobotID rid r)
_ -> return r
let rid = r' ^. robotID
robotMap %= IM.insert rid r'
robotsByLocation
%= M.insertWith S.union (r' ^. robotLocation) (S.singleton (r' ^. robotName))
internalActiveRobots %= S.insert (r' ^. robotName)
return r'
%= M.insertWith IS.union (r' ^. robotLocation) (IS.singleton rid)
internalActiveRobots %= IS.insert rid
-- | What type of game does the user want to start?
data GameType
@ -410,6 +410,14 @@ initGameState gtype = do
liftIO $ putStrLn "Loading recipes..."
recipes <- loadRecipes entities >>= (`isRightOr` id)
(adjs, names) <- liftIO $ do
putStrLn "Loading name generation data..."
adjsFile <- getDataFileName "adjectives.txt"
as <- tail . T.lines <$> T.readFile adjsFile
namesFile <- getDataFileName "names.txt"
ns <- tail . T.lines <$> T.readFile namesFile
return (as, ns)
iGameType <- instGameType entities recipes gtype
let baseDeviceNames =
@ -422,7 +430,8 @@ initGameState gtype = do
, "logger"
]
baseDevices = mapMaybe (`lookupEntityName` entities) baseDeviceNames
baseName = "base"
-- baseName = "base"
baseID = 0
theBase = baseRobot baseDevices
robotList = case iGameType of
@ -455,24 +464,26 @@ initGameState gtype = do
{ _creativeMode = creative
, _winCondition = theWinCondition
, _runStatus = Running
, _robotMap = M.fromList $ map (view robotName &&& id) robotList
, _robotMap = IM.fromList $ map (view robotID &&& id) robotList
, _robotsByLocation =
M.fromListWith S.union $
map (view robotLocation &&& (S.singleton . view robotName)) robotList
, _activeRobots = setOf (traverse . robotName) robotList
M.fromListWith IS.union $
map (view robotLocation &&& (IS.singleton . view robotID)) robotList
, _activeRobots = setOf (traverse . robotID) robotList
, _waitingRobots = M.empty
, _gensym = 0
, _randGen = mkStdGen seed
, _adjList = listArray (0, length adjs - 1) adjs
, _nameList = listArray (0, length names - 1) names
, _entityMap = entities
, _recipesOut = outRecipeMap recipes
, _recipesIn = inRecipeMap recipes
, _world = theWorld
, _viewCenterRule = VCRobot baseName
, _viewCenterRule = VCRobot baseID
, _viewCenter = V2 0 0
, _needsRedraw = False
, _replStatus = REPLDone
, _messageQueue = []
, _focusedRobotName = baseName
, _focusedRobotID = baseID
, _ticks = 0
}
where
@ -494,18 +505,18 @@ ticks :: Lens' GameState Integer
-- | Takes a robot out of the activeRobots set and puts it in the waitingRobots
-- queue.
sleepUntil :: Has (State GameState) sig m => Text -> Integer -> m ()
sleepUntil rn time = do
internalActiveRobots %= S.delete rn
waitingRobots . at time . non [] %= (rn :)
sleepUntil :: Has (State GameState) sig m => RID -> Integer -> m ()
sleepUntil rid time = do
internalActiveRobots %= IS.delete rid
waitingRobots . at time . non [] %= (rid :)
-- | Takes a robot out of the activeRobots set.
sleepForever :: Has (State GameState) sig m => Text -> m ()
sleepForever rn = internalActiveRobots %= S.delete rn
sleepForever :: Has (State GameState) sig m => RID -> m ()
sleepForever rid = internalActiveRobots %= IS.delete rid
-- | Adds a robot to the activeRobots set.
activateRobot :: Has (State GameState) sig m => Text -> m ()
activateRobot rn = internalActiveRobots %= S.insert rn
activateRobot :: Has (State GameState) sig m => RID -> m ()
activateRobot rid = internalActiveRobots %= IS.insert rid
-- | Removes robots whose wake up time matches the current game ticks count
-- from the waitingRobots queue and put them back in the activeRobots set
@ -513,18 +524,18 @@ activateRobot rn = internalActiveRobots %= S.insert rn
wakeUpRobotsDoneSleeping :: Has (State GameState) sig m => m ()
wakeUpRobotsDoneSleeping = do
time <- use ticks
mrns <- waitingRobots . at time <<.= Nothing
case mrns of
mrids <- waitingRobots . at time <<.= Nothing
case mrids of
Nothing -> return ()
Just rns -> do
Just rids -> do
robots <- use robotMap
let aliveRns = filter (`M.member` robots) rns
internalActiveRobots %= S.union (S.fromList aliveRns)
let aliveRids = filter (`IM.member` robots) rids
internalActiveRobots %= IS.union (IS.fromList aliveRids)
deleteRobot :: Has (State GameState) sig m => Text -> m ()
deleteRobot :: Has (State GameState) sig m => RID -> m ()
deleteRobot rn = do
internalActiveRobots %= S.delete rn
internalActiveRobots %= IS.delete rn
mrobot <- robotMap . at rn <<.= Nothing
mrobot `forM_` \robot -> do
-- Delete the robot from the index of robots by location.
robotsByLocation . ix (robot ^. robotLocation) %= S.delete rn
robotsByLocation . ix (robot ^. robotLocation) %= IS.delete rn

View File

@ -19,15 +19,18 @@
-- interpreter for the Swarm language.
module Swarm.Game.Step where
import Control.Lens hiding (Const, from, parts, use, uses, view, (%=), (+=), (.=), (<>=))
import Control.Monad (forM_, guard, msum, unless, void, when)
import Control.Lens hiding (Const, from, parts, use, uses, view, (%=), (+=), (.=), (<+=), (<>=))
import Control.Monad (forM_, guard, msum, unless, when)
import Data.Array (bounds, (!))
import Data.Bool (bool)
import Data.Either (rights)
import Data.Int (Int64)
import qualified Data.IntMap as IM
import qualified Data.IntSet as IS
import Data.List (find)
import qualified Data.List as L
import qualified Data.Map as M
import Data.Maybe (isNothing, listToMaybe, mapMaybe)
import Data.Maybe (fromMaybe, isNothing, listToMaybe, mapMaybe)
import qualified Data.Sequence as Seq
import qualified Data.Set as S
import Data.Text (Text)
@ -76,8 +79,8 @@ gameTick :: (Has (State GameState) sig m, Has (Lift IO) sig m) => m ()
gameTick = do
wakeUpRobotsDoneSleeping
robotNames <- use activeRobots
forM_ robotNames $ \rn -> do
mr <- uses robotMap (M.lookup rn)
forM_ (IS.toList robotNames) $ \rn -> do
mr <- uses robotMap (IM.lookup rn)
case mr of
Nothing -> return ()
Just curRobot -> do
@ -85,7 +88,7 @@ gameTick = do
if curRobot' ^. selfDestruct
then deleteRobot rn
else do
robotMap %= M.insert rn curRobot'
robotMap %= IM.insert rn curRobot'
let oldLoc = curRobot ^. robotLocation
newLoc = curRobot' ^. robotLocation
@ -95,14 +98,14 @@ gameTick = do
-- visited!
deleteOne _ Nothing = Nothing
deleteOne x (Just s)
| S.null s' = Nothing
| IS.null s' = Nothing
| otherwise = Just s'
where
s' = S.delete x s
s' = IS.delete x s
when (newLoc /= oldLoc) $ do
robotsByLocation . at oldLoc %= deleteOne rn
robotsByLocation . at newLoc . non Empty %= S.insert rn
robotsByLocation . at newLoc . non Empty %= IS.insert rn
case waitingUntil curRobot' of
Just wakeUpTime ->
sleepUntil rn wakeUpTime
@ -114,7 +117,7 @@ gameTick = do
-- the result in the game state so it can be displayed by the REPL;
-- also save the current store into the robotContext so we can
-- restore it the next time we start a computation.
mr <- use (robotMap . at "base")
mr <- use (robotMap . at 0)
case mr of
Just r -> do
res <- use replStatus
@ -122,7 +125,7 @@ gameTick = do
REPLWorking ty Nothing -> case getResult r of
Just (v, s) -> do
replStatus .= REPLWorking ty (Just v)
robotMap . ix "base" . robotContext . defStore .= s
robotMap . ix 0 . robotContext . defStore .= s
Nothing -> return ()
_otherREPLStatus -> return ()
Nothing -> return ()
@ -156,7 +159,7 @@ evaluateCESK ::
m Value
evaluateCESK cesk = evalState r . runCESK $ cesk
where
r = mkRobot "" zero zero cesk [] & systemRobot .~ True
r = mkRobot (-1) Nothing "" [] zero zero defaultRobotDisplay cesk [] [] True
runCESK ::
( Has (Lift IO) sig m
@ -197,9 +200,9 @@ updateEntityAt ::
(Has (State GameState) sig m) => V2 Int64 -> (Maybe Entity -> Maybe Entity) -> m ()
updateEntityAt loc upd = zoomWorld (W.updateM @Int (W.locToCoords loc) upd)
-- | Get the robot with a given name (if any).
robotNamed :: (Has (State GameState) sig m) => Text -> m (Maybe Robot)
robotNamed nm = use (robotMap . at nm)
-- | Get the robot with a given ID.
robotWithID :: (Has (State GameState) sig m) => RID -> m (Maybe Robot)
robotWithID rid = use (robotMap . at rid)
-- | Manhattan distance between world locations.
manhattan :: V2 Int64 -> V2 Int64 -> Int64
@ -214,6 +217,50 @@ uniform bnds = do
randGen .= g
return n
-- | Generate a robot with a unique ID number.
genRobot ::
(Has (State GameState) sig m) =>
-- | ID of the robot's parent, if it has one.
Maybe RID ->
-- | Name of the robot.
Text ->
-- | Initial location.
V2 Int64 ->
-- | Initial heading/direction.
V2 Int64 ->
-- | Initial CESK machine.
CESK ->
-- | Installed devices.
[Entity] ->
-- | Should this be a system robot?
Bool ->
m Robot
genRobot mp name l d m devs sys = do
rid <- gensym <+= 1
let robot =
mkRobot
rid
mp
name
["A generic robot."]
l
d
defaultRobotDisplay
m
devs
[]
sys
return robot
-- | Generate a random robot name in the form adjective_name.
randomName :: Has (State GameState) sig m => m Text
randomName = do
adjs <- use @GameState adjList
names <- use @GameState nameList
i <- uniform (bounds adjs)
j <- uniform (bounds names)
return $ T.concat [adjs ! i, "_", names ! j]
------------------------------------------------------------
-- Debugging
------------------------------------------------------------
@ -345,8 +392,12 @@ stepCESK cesk = case cesk of
return $ Up (Fatal (T.append "Antiquoted variable found at runtime: $str:" v)) s k
In (TAntiInt v) _ s k ->
return $ Up (Fatal (T.append "Antiquoted variable found at runtime: $int:" v)) s k
-- A constant is turned into a VCApp which might be waiting for arguments.
In (TConst c) _ s k -> return $ Out (VCApp c []) s k
-- Function constants of arity 0 are evaluated immediately
-- (e.g. parent, self). Any other constant is turned into a VCApp,
-- which is waiting for arguments and/or an FExec frame.
In (TConst c) _ s k
| arity c == 0 && not (isCmd c) -> evalConst c [] s k
| otherwise -> return $ Out (VCApp c []) s k
-- To evaluate a variable, just look it up in the context.
In (TVar x) e s k -> withExceptions s k $ do
v <-
@ -443,20 +494,24 @@ stepCESK cesk = case cesk of
-- command, and remember the second for execution later.
Out (VBind mx c1 c2 e) s (FExec : k) -> return $ In c1 e s (FExec : FBind mx c2 e : k)
-- If first command completes with a value along with an environment
-- resulting from definition commands, switch to evaluating the
-- second command of the bind. Extend the environment with both the
-- definition environment resulting from the first command, as well
-- as a binding for the result (if the bind was of the form @x <-
-- c1; c2@). Remember that we must execute the second command once
-- it has been evaluated, then union any resulting definition
-- environment with the definition environment from the first
-- command.
Out (VResult v ve) s (FBind mx t2 e : k) ->
return $ In t2 (maybe id (`addBinding` v) mx . (`union` ve) $ e) s (FExec : FUnionEnv ve : k)
-- On the other hand, if the first command completes with a simple value,
-- we do something similar, but don't have to worry about the environment.
Out v s (FBind mx t2 e : k) ->
return $ In t2 (maybe id (`addBinding` v) mx e) s (FExec : k)
-- resulting from definition commands and/or binds, switch to
-- evaluating the second command of the bind. Extend the
-- environment with both the environment resulting from the first
-- command, as well as a binding for the result (if the bind was of
-- the form @x <- c1; c2@). Remember that we must execute the
-- second command once it has been evaluated, then union any
-- resulting definition environment with the definition environment
-- from the first command.
Out (VResult v ve) s (FBind mx t2 e : k) -> do
let ve' = maybe id (`addBinding` v) mx ve
return $ In t2 (e `union` ve') s (FExec : FUnionEnv ve' : k)
-- If the first command completes with a simple value and there is no binder,
-- then we just continue without worrying about the environment.
Out _ s (FBind Nothing t2 e : k) -> return $ In t2 e s (FExec : k)
-- If the first command completes with a simple value and there is a binder,
-- we promote it to the returned environment as well.
Out v s (FBind (Just x) t2 e : k) -> do
return $ In t2 (addBinding x v e) s (FExec : FUnionEnv (singleton x v) : k)
-- If a command completes with a value and definition environment,
-- and the next continuation frame contains a previous environment
-- to union with, then pass the unioned environments along in
@ -559,23 +614,29 @@ seedProgram minTime randTime thing =
selfdestruct
|]
-- | Construct a "seed robot" from entity, time range and position.
-- It has low priority and will be covered by placed entities.
mkSeedBot :: Entity -> (Integer, Integer) -> V2 Int64 -> Robot
mkSeedBot e (minT, maxT) loc =
mkRobot
"seed"
loc
(V2 0 0)
(initMachine (seedProgram minT (maxT - minT) (e ^. entityName)) empty emptyStore)
[]
& robotDisplay
.~ ( defaultEntityDisplay '.'
& displayAttr .~ (e ^. entityDisplay . displayAttr)
& displayPriority .~ 0
)
& robotInventory .~ E.singleton e
& systemRobot .~ True
-- | Construct a "seed robot" from entity, time range and position,
-- and add it to the world. It has low priority and will be covered
-- by placed entities.
addSeedBot :: Has (State GameState) sig m => Entity -> (Integer, Integer) -> V2 Int64 -> m ()
addSeedBot e (minT, maxT) loc = do
r <-
genRobot
Nothing
"seed"
loc
(V2 0 0)
(initMachine (seedProgram minT (maxT - minT) (e ^. entityName)) empty emptyStore)
[]
True
let r' =
r
& robotDisplay
.~ ( defaultEntityDisplay '.'
& displayAttr .~ (e ^. entityDisplay . displayAttr)
& displayPriority .~ 0
)
& robotInventory .~ E.singleton e
addRobot r'
-- | Interpret the execution (or evaluation) of a constant application
-- to some values.
@ -647,7 +708,7 @@ execConst c vs s k = do
then -- Special case: if the time is zero, growth is instant.
updateEntityAt loc (const (Just e))
else -- Otherwise, grow a new entity from a seed.
void $ addRobot $ mkSeedBot e (minT, maxT) loc
addSeedBot e (minT, maxT) loc
-- Add the picked up item to the robot's inventory. If the
-- entity yields something different, add that instead.
@ -691,16 +752,16 @@ execConst c vs s k = do
return $ Out VUnit s k
_ -> badConst
Give -> case vs of
[VString otherName, VString itemName] -> do
[VRobot otherID, VString itemName] -> do
-- Make sure the other robot exists
other <-
robotNamed otherName
>>= (`isJustOrFail` ["There is no robot named", otherName, "."])
robotWithID otherID
>>= (`isJustOrFail` ["There is no robot with ID", from (show otherID), "."])
-- Make sure it is in the same location
loc <- use robotLocation
((other ^. robotLocation) `manhattan` loc <= 1)
`holdsOrFail` ["The robot named", otherName, "is not close enough."]
`holdsOrFail` ["The robot with ID", from (show otherID), "is not close enough."]
-- Make sure we have the required item
inv <- use robotInventory
@ -717,29 +778,29 @@ execConst c vs s k = do
-- return a modified Robot which gets put back in the
-- robotMap, overwriting any changes to this robot made
-- directly in the robotMap during the tick.
myName <- use robotName
focusedName <- use focusedRobotName
when (otherName /= myName) $ do
myID <- use robotID
focusedID <- use focusedRobotID
when (otherID /= myID) $ do
-- Make the exchange
robotMap . at otherName . _Just . robotInventory %= insert item
robotMap . at otherID . _Just . robotInventory %= insert item
robotInventory %= delete item
-- Flag the UI for a redraw if we are currently showing either robot's inventory
when (focusedName == myName || focusedName == otherName) flagRedraw
when (focusedID == myID || focusedID == otherID) flagRedraw
return $ Out VUnit s k
_ -> badConst
Install -> case vs of
[VString otherName, VString itemName] -> do
[VRobot otherID, VString itemName] -> do
-- Make sure the other robot exists
other <-
robotNamed otherName
>>= (`isJustOrFail` ["There is no robot named", otherName, "."])
robotWithID otherID
>>= (`isJustOrFail` ["There is no robot with ID", from (show otherID), "."])
-- Make sure it is in the same location
loc <- use robotLocation
((other ^. robotLocation) `manhattan` loc <= 1)
`holdsOrFail` ["The robot named", otherName, "is not close enough."]
`holdsOrFail` ["The robot with ID", from (show otherID), "is not close enough."]
-- Make sure we have the required item
inv <- use robotInventory
@ -750,9 +811,9 @@ execConst c vs s k = do
(E.lookup item inv > 0)
`holdsOrFail` ["You don't have", indefinite itemName, "to install."]
myName <- use robotName
focusedName <- use focusedRobotName
case otherName == myName of
myID <- use robotID
focusedID <- use focusedRobotID
case otherID == myID of
-- We have to special case installing something on ourselves
-- for the same reason as Give.
True -> do
@ -763,17 +824,17 @@ execConst c vs s k = do
robotInventory %= delete item
-- Flag the UI for a redraw if we are currently showing our inventory
when (focusedName == myName) flagRedraw
when (focusedID == myID) flagRedraw
False -> do
let otherDevices = robotMap . at otherName . _Just . installedDevices
let otherDevices = robotMap . at otherID . _Just . installedDevices
already <- use $ pre (otherDevices . to (`E.contains` item))
unless (already == Just True) $ do
robotMap . at otherName . _Just . installedDevices %= insert item
robotMap . at otherID . _Just . installedDevices %= insert item
robotInventory %= delete item
-- Flag the UI for a redraw if we are currently showing
-- either robot's inventory
when (focusedName == myName || focusedName == otherName) flagRedraw
when (focusedID == myID || focusedID == otherID) flagRedraw
return $ Out VUnit s k
_ -> badConst
@ -877,25 +938,25 @@ execConst c vs s k = do
return $ Out res s k
_ -> badConst
Upload -> case vs of
[VString otherName] -> do
[VRobot otherID] -> do
-- Make sure the other robot exists
other <-
robotNamed otherName
>>= (`isJustOrFail` ["There is no robot named", otherName, "."])
robotWithID otherID
>>= (`isJustOrFail` ["There is no robot with ID", from (show otherID), "."])
-- Make sure it is in the same location
loc <- use robotLocation
((other ^. robotLocation) `manhattan` loc <= 1)
`holdsOrFail` ["The robot named", otherName, "is not close enough."]
`holdsOrFail` ["The robot with ID", from (show otherID), "is not close enough."]
-- Upload knowledge of everything in our inventory
inv <- use robotInventory
forM_ (elems inv) $ \(_, e) ->
robotMap . at otherName . _Just . robotInventory %= insertCount 0 e
robotMap . at otherID . _Just . robotInventory %= insertCount 0 e
-- Upload our log
rlog <- use robotLog
robotMap . at otherName . _Just . robotLog <>= rlog
robotMap . at otherID . _Just . robotLog <>= rlog
return $ Out VUnit s k
_ -> badConst
@ -905,9 +966,9 @@ execConst c vs s k = do
return $ Out (VInt n) s k
_ -> badConst
As -> case vs of
[VString name, prog] -> do
[VRobot rid, prog] -> do
-- Get the named robot and current game state
r <- robotNamed name >>= (`isJustOrFail` ["There is no robot named ", name])
r <- robotWithID rid >>= (`isJustOrFail` ["There is no robot with ID", from (show rid)])
g <- get @GameState
-- Execute the given program *hypothetically*: i.e. in a fresh
@ -926,7 +987,7 @@ execConst c vs s k = do
_ -> badConst
Say -> case vs of
[VString msg] -> do
rn <- use robotName
rn <- use robotName -- XXX use robot name + ID
emitMessage (T.concat [rn, ": ", msg])
return $ Out VUnit s k
_ -> badConst
@ -938,16 +999,16 @@ execConst c vs s k = do
return $ Out VUnit s k
_ -> badConst
View -> case vs of
[VString name] -> do
[VRobot rid] -> do
_ <-
robotNamed name
>>= (`isJustOrFail` ["There is no robot named ", name, " to view."])
robotWithID rid
>>= (`isJustOrFail` ["There is no robot with ID", from (show rid), "to view."])
-- Only the base can actually change the view in the UI. Other robots can
-- execute this command but it does nothing (at least for now).
rn <- use robotName
when (rn == "base") $
viewCenterRule .= VCRobot name
rn <- use robotID
when (rn == 0) $
viewCenterRule .= VCRobot rid
return $ Out VUnit s k
_ -> badConst
@ -986,11 +1047,24 @@ execConst c vs s k = do
Nothing -> return $ Out (VBool False) s k
Just e -> return $ Out (VBool (T.toLower (e ^. entityName) == T.toLower name)) s k
_ -> badConst
Self -> do
rid <- use robotID
return $ Out (VRobot rid) s k
Parent -> do
mp <- use robotParentID
rid <- use robotID
return $ Out (VRobot (fromMaybe rid mp)) s k
Base -> return $ Out (VRobot 0) s k
Whoami -> case vs of
[] -> do
name <- use robotName
return $ Out (VString name) s k
_ -> badConst
Setname -> case vs of
[VString name] -> do
robotName .= name
return $ Out VUnit s k
_ -> badConst
Force -> case vs of
[VDelay t e] -> return $ In t e s k
[VRef loc] ->
@ -1015,6 +1089,11 @@ execConst c vs s k = do
Just Blackhole {} -> return $ Up InfiniteLoop s k
-- If the location already contains a value, just return it.
Just (V v) -> return $ Out v s k
-- If a force is applied to any other kind of value, just ignore it.
-- This is needed because of the way we wrap all free variables in @force@
-- in case they come from a @def@ which are always wrapped in @delay@.
-- But binders (i.e. @x <- ...@) are also exported to the global context.
[v] -> return $ Out v s k
_ -> badConst
If -> case vs of
-- Use the boolean to pick the correct branch, and apply @force@ to it.
@ -1042,19 +1121,19 @@ execConst c vs s k = do
[VString msg] -> return $ Up (User msg) s k
_ -> badConst
Reprogram -> case vs of
[VString childRobotName, VDelay cmd e] -> do
[VRobot childRobotID, VDelay cmd e] -> do
r <- get
em <- use entityMap
creative <- use creativeMode
-- check if robot exists
childRobot <-
robotNamed childRobotName
>>= (`isJustOrFail` ["There is no robot named", childRobotName, "."])
robotWithID childRobotID
>>= (`isJustOrFail` ["There is no robot with ID", from (show childRobotID), "."])
-- check that current robot is not trying to reprogram self
myName <- use robotName
(childRobotName /= myName)
myID <- use robotID
(childRobotID /= myID)
`holdsOrFail` ["You cannot make a robot reprogram itself"]
-- check if robot has completed executing it's current command
@ -1090,9 +1169,9 @@ execConst c vs s k = do
-- the childRobot inherits the parent robot's environment
-- and context which collectively mean all the variables
-- declared in the parent robot
robotMap . at childRobotName . _Just . machine .= In cmd e s [FExec]
robotMap . at childRobotName . _Just . robotContext .= r ^. robotContext
activateRobot childRobotName
robotMap . at childRobotID . _Just . machine .= In cmd e s [FExec]
robotMap . at childRobotID . _Just . robotContext .= r ^. robotContext
activateRobot childRobotID
return $ Out VUnit s k
_ -> badConst
@ -1119,11 +1198,13 @@ execConst c vs s k = do
-- and figure out how to do capability checking on Values (which
-- would return the capabilities needed to *execute* them),
-- hopefully without duplicating too much code.
[VString name, VDelay cmd e] -> do
[VDelay cmd e] -> do
r <- get
em <- use entityMap
creative <- use creativeMode
pid <- use robotID
let -- Standard devices that are always installed.
-- XXX in the future, make a way to build these and just start the base
-- out with a large supply of each?
@ -1159,19 +1240,24 @@ execConst c vs s k = do
, commaList (map (^. entityName) (S.toList missingDevices))
]
-- Construct the new robot.
let newRobot =
mkRobot
name
(r ^. robotLocation)
( ((r ^. robotOrientation) >>= \dir -> guard (dir /= zero) >> return dir)
? east
)
(In cmd e s [FExec])
(S.toList devices)
-- Pick a random display name.
displayName <- randomName
-- Add the new robot to the world.
newRobot' <- addRobot newRobot
-- Construct the new robot.
newRobot <-
genRobot
(Just pid)
displayName
(r ^. robotLocation)
( ((r ^. robotOrientation) >>= \dir -> guard (dir /= zero) >> return dir)
? east
)
(In cmd e s [FExec])
(S.toList devices)
False
-- Add it to the world.
addRobot newRobot
-- Remove from the inventory any devices which were installed on the new robot,
-- if not in creative mode.
@ -1181,16 +1267,16 @@ execConst c vs s k = do
-- Flag the world for a redraw and return the name of the newly constructed robot.
flagRedraw
return $ Out (VString (newRobot' ^. robotName)) s k
return $ Out (VRobot (newRobot ^. robotID)) s k
_ -> badConst
Salvage -> case vs of
[] -> do
loc <- use robotLocation
rm <- use robotMap
robotSet <- use (robotsByLocation . at loc)
let robotNameList = maybe [] S.toList robotSet
mtarget = find okToSalvage . mapMaybe (`M.lookup` rm) $ robotNameList
okToSalvage r = (r ^. robotName /= "base") && (not . isActive $ r)
let robotIDList = maybe [] IS.toList robotSet
mtarget = find okToSalvage . mapMaybe (`IM.lookup` rm) $ robotIDList
okToSalvage r = (r ^. robotID /= 0) && (not . isActive $ r)
case mtarget of
Nothing -> return $ Out VUnit s k -- Nothing to salvage
Just target -> do
@ -1208,7 +1294,7 @@ execConst c vs s k = do
when (creative || inst `E.contains` logger) $ robotLog <>= target ^. robotLog
-- Finally, delete the salvaged robot
deleteRobot (target ^. robotName)
deleteRobot (target ^. robotID)
return $ Out VUnit s k
_ -> badConst
@ -1330,6 +1416,7 @@ compareValues = \v1 -> case v1 of
VString t1 -> \case VString t2 -> return (compare t1 t2); v2 -> incompatCmp v1 v2
VDir d1 -> \case VDir d2 -> return (compare d1 d2); v2 -> incompatCmp v1 v2
VBool b1 -> \case VBool b2 -> return (compare b1 b2); v2 -> incompatCmp v1 v2
VRobot r1 -> \case VRobot r2 -> return (compare r1 r2); v2 -> incompatCmp v1 v2
VInj s1 v1' -> \case
VInj s2 v2' ->
case compare s1 s2 of

View File

@ -43,6 +43,8 @@ data Value where
VDir :: Direction -> Value
-- | A boolean.
VBool :: Bool -> Value
-- | A reference to a robot.
VRobot :: Int -> Value
-- | An injection into a sum type. False = left, True = right.
VInj :: Bool -> Value -> Value
-- | A pair.
@ -93,6 +95,7 @@ valueToTerm (VInt n) = TInt n
valueToTerm (VString s) = TString s
valueToTerm (VDir d) = TDir d
valueToTerm (VBool b) = TBool b
valueToTerm (VRobot r) = TRobot r
valueToTerm (VInj s v) = TApp (TConst (bool Inl Inr s)) (valueToTerm v)
valueToTerm (VPair v1 v2) = TPair (valueToTerm v1) (valueToTerm v2)
valueToTerm (VClo x t e) =
@ -105,9 +108,7 @@ valueToTerm (VDef r x t _) = TDef r x Nothing t
valueToTerm (VResult v _) = valueToTerm v
valueToTerm (VBind mx c1 c2 _) = TBind mx c1 c2
valueToTerm (VDelay t _) = TDelay SimpleDelay t
valueToTerm (VRef n) = TInt (fromIntegral n) -- XXX WRONG
-- We really can't get away with valueToTerm any more, we need to make a proper
-- pretty-printer for values.
valueToTerm (VRef n) = TRef n
-- | An environment is a mapping from variable names to values.
type Env = Ctx Value

View File

@ -97,8 +97,10 @@ data Capability
CRecursion
| -- | Execute the 'Reprogram' command
CReprogram
| -- | Capability to introspect and see it's own name
| -- | Capability to introspect and see its own name
CWhoami
| -- | Capability to set its own name
CSetname
| -- | God-like capabilities. For e.g. commands intended only for
-- checking challenge mode win conditions, and not for use by
-- players.
@ -236,6 +238,10 @@ constCaps =
AppF -> []
Force -> []
Return -> []
Self -> []
Parent -> []
Base -> []
Setname -> []
-- Some straightforward ones.
Log -> [CLog]
Selfdestruct -> [CSelfdestruct]

View File

@ -82,6 +82,7 @@ reservedWords =
, "string"
, "dir"
, "bool"
, "robot"
, "cmd"
, "delay"
, "let"
@ -197,6 +198,7 @@ parseTypeAtom =
<|> TyString <$ reserved "string"
<|> TyDir <$ reserved "dir"
<|> TyBool <$ reserved "bool"
<|> TyRobot <$ reserved "robot"
<|> TyCmd <$> (reserved "cmd" *> parseTypeAtom)
<|> TyDelay <$> braces parseType
<|> parens parseType
@ -283,7 +285,7 @@ parseTerm = sepEndBy1 parseStmt (symbol ";") >>= mkBindChain
mkBindChain :: [Stmt] -> Parser Syntax
mkBindChain stmts = case last stmts of
Binder _ _ -> fail "Last command in a chain must not have a binder"
Binder x _ -> return $ foldr mkBind (STerm (TApp (TConst Return) (TVar x))) stmts
BareTerm t -> return $ foldr mkBind t (init stmts)
where
mkBind (BareTerm t1) t2 = loc t1 t2 $ SBind Nothing t1 t2

View File

@ -65,6 +65,7 @@ instance PrettyPrec BaseTy where
prettyPrec _ BDir = "dir"
prettyPrec _ BString = "string"
prettyPrec _ BBool = "bool"
prettyPrec _ BRobot = "robot"
instance PrettyPrec IntVar where
prettyPrec _ = pretty . mkVarName "u"
@ -119,6 +120,8 @@ instance PrettyPrec Term where
prettyPrec _ (TString s) = fromString (show s)
prettyPrec _ (TAntiString v) = "$str:" <> pretty v
prettyPrec _ (TBool b) = bool "false" "true" b
prettyPrec _ (TRobot r) = "<r" <> pretty r <> ">"
prettyPrec _ (TRef r) = "@" <> pretty r
prettyPrec _ (TVar s) = pretty s
prettyPrec _ (TDelay _ t) = braces $ ppr t
prettyPrec _ t@TPair {} = prettyTuple t
@ -191,3 +194,5 @@ instance PrettyPrec TypeErr where
"Infinite type:" <+> ppr x <+> "=" <+> ppr uty
prettyPrec _ (DefNotTopLevel _ t) =
"Definitions may only be at the top level:" <+> ppr t
prettyPrec _ (CantInfer _ t) =
"Couldn't infer the type of term (this shouldn't happen; please report this as a bug!):" <+> ppr t

View File

@ -254,8 +254,16 @@ data Const
Upload
| -- | See if a specific entity is here. (This may be removed.)
Ishere
| -- | Find it's own name
| -- | Get a reference to oneself
Self
| -- | Get the robot's parent
Parent
| -- | Get a reference to the base
Base
| -- | Get the robot's display name
Whoami
| -- | Set the robot's display name
Setname
| -- | Get a uniformly random integer.
Random
| -- Modules
@ -430,7 +438,11 @@ constInfo c = case c of
Scan -> commandLow 0
Upload -> commandLow 1
Ishere -> commandLow 1
Self -> functionLow 0
Parent -> functionLow 0
Base -> functionLow 0
Whoami -> commandLow 0
Setname -> commandLow 1
Random -> commandLow 1
Run -> commandLow 1
Return -> commandLow 1
@ -579,6 +591,13 @@ data Term
TAntiString Text
| -- | A Boolean literal.
TBool Bool
| -- | A robot value. These never show up in surface syntax, but are
-- here so we can factor pretty-printing for Values through
-- pretty-printing for Terms.
TRobot Int
| -- | A memory reference. These likewise never show up in surface syntax,
-- but are here to facilitate pretty-printing.
TRef Int
| -- | A variable.
TVar Var
| -- | A pair.
@ -625,6 +644,8 @@ fvT f = go S.empty
TString {} -> pure t
TAntiString {} -> pure t
TBool {} -> pure t
TRobot {} -> pure t
TRef {} -> pure t
TVar x
| x `S.member` bound -> pure t
| otherwise -> f (TVar x)

View File

@ -229,6 +229,9 @@ data TypeErr
Mismatch Location (TypeF UType) (TypeF UType)
| -- | A definition was encountered not at the top level.
DefNotTopLevel Location Term
| -- | A term was encountered which we cannot infer the type of.
-- This should never happen.
CantInfer Location Term
deriving (Show)
instance Fallible TypeF IntVar TypeErr where
@ -242,6 +245,7 @@ getTypeErrLocation te = case te of
Infinite _ _ -> Nothing
Mismatch l _ _ -> Just l
DefNotTopLevel l _ -> Just l
CantInfer l _ -> Just l
------------------------------------------------------------
-- Type inference / checking
@ -301,8 +305,12 @@ inferModule s@(Syntax _ t) = (`catchError` addLocToTypeErr s) $ case t of
_ <- decomposeCmdTy cmdb
-- Ctx.union is right-biased, so ctx1 `union` ctx2 means later
-- definitions will shadow previous ones.
return $ Module cmdb (ctx1 `Ctx.union` ctx2)
-- definitions will shadow previous ones. Include the binder
-- (if any) as well, since binders are made available at the top
-- level, just like definitions. e.g. if the user writes `r <- build {move}`,
-- then they will be able to refer to r again later.
let ctxX = maybe Ctx.empty (`Ctx.singleton` Forall [] a) mx
return $ Module cmdb (ctx1 `Ctx.union` ctxX `Ctx.union` ctx2)
-- In all other cases, there can no longer be any definitions in the
-- term, so delegate to 'infer'.
@ -319,6 +327,11 @@ infer s@(Syntax l t) = (`catchError` addLocToTypeErr s) $ case t of
TString _ -> return UTyString
TAntiString _ -> return UTyString
TBool _ -> return UTyBool
TRobot _ -> return UTyRobot
-- We should never encounter a TRef since they do not show up in
-- surface syntax, only as values while evaluating (*after*
-- typechecking).
TRef _ -> throwError $ CantInfer l t
-- To infer the type of a pair, just infer both components.
SPair t1 t2 -> UTyProd <$> infer t1 <*> infer t2
-- if t : ty, then {t} : {ty}.
@ -429,26 +442,30 @@ inferConst c = toU $ case c of
Turn -> [tyQ| dir -> cmd () |]
Grab -> [tyQ| cmd string |]
Place -> [tyQ| string -> cmd () |]
Give -> [tyQ| string -> string -> cmd () |]
Install -> [tyQ| string -> string -> cmd () |]
Give -> [tyQ| robot -> string -> cmd () |]
Install -> [tyQ| robot -> string -> cmd () |]
Make -> [tyQ| string -> cmd () |]
Has -> [tyQ| string -> cmd bool |]
Count -> [tyQ| string -> cmd int |]
Reprogram -> [tyQ| string -> {cmd a} -> cmd () |]
Build -> [tyQ| string -> {cmd a} -> cmd string |]
Reprogram -> [tyQ| robot -> {cmd a} -> cmd () |]
Build -> [tyQ| {cmd a} -> cmd robot |]
Drill -> [tyQ| dir -> cmd () |]
Salvage -> [tyQ| cmd () |]
Say -> [tyQ| string -> cmd () |]
Log -> [tyQ| string -> cmd () |]
View -> [tyQ| string -> cmd () |]
View -> [tyQ| robot -> cmd () |]
Appear -> [tyQ| string -> cmd () |]
Create -> [tyQ| string -> cmd () |]
Whereami -> [tyQ| cmd (int * int) |]
Blocked -> [tyQ| cmd bool |]
Scan -> [tyQ| dir -> cmd (() + string) |]
Upload -> [tyQ| string -> cmd () |]
Upload -> [tyQ| robot -> cmd () |]
Ishere -> [tyQ| string -> cmd bool |]
Self -> [tyQ| robot |]
Parent -> [tyQ| robot |]
Base -> [tyQ| robot |]
Whoami -> [tyQ| cmd string |]
Setname -> [tyQ| string -> cmd () |]
Random -> [tyQ| int -> cmd int |]
Run -> [tyQ| string -> cmd () |]
If -> [tyQ| bool -> {a} -> {a} -> a |]
@ -477,7 +494,7 @@ inferConst c = toU $ case c of
Format -> [tyQ| a -> string |]
Concat -> [tyQ| string -> string -> string |]
AppF -> [tyQ| (a -> b) -> a -> b |]
As -> [tyQ| string -> {cmd a} -> cmd a |]
As -> [tyQ| robot -> {cmd a} -> cmd a |]
where
cmpBinT = [tyQ| a -> a -> bool |]
arithBinT = [tyQ| int -> int -> int |]

View File

@ -40,6 +40,7 @@ module Swarm.Language.Types (
pattern TyString,
pattern TyDir,
pattern TyBool,
pattern TyRobot,
pattern (:+:),
pattern (:*:),
pattern (:->:),
@ -55,6 +56,7 @@ module Swarm.Language.Types (
pattern UTyString,
pattern UTyDir,
pattern UTyBool,
pattern UTyRobot,
pattern UTySum,
pattern UTyProd,
pattern UTyFun,
@ -116,6 +118,8 @@ data BaseTy
BDir
| -- | Booleans.
BBool
| -- | Robots.
BRobot
deriving (Eq, Ord, Show, Data)
-- | A "structure functor" encoding the shape of type expressions.
@ -312,6 +316,9 @@ pattern TyDir = Fix (TyBaseF BDir)
pattern TyBool :: Type
pattern TyBool = Fix (TyBaseF BBool)
pattern TyRobot :: Type
pattern TyRobot = Fix (TyBaseF BRobot)
infixr 5 :+:
pattern (:+:) :: Type -> Type -> Type
@ -354,6 +361,9 @@ pattern UTyDir = UTerm (TyBaseF BDir)
pattern UTyBool :: UType
pattern UTyBool = UTerm (TyBaseF BBool)
pattern UTyRobot :: UType
pattern UTyRobot = UTerm (TyBaseF BRobot)
pattern UTySum :: UType -> UType -> UType
pattern UTySum ty1 ty2 = UTerm (TySumF ty1 ty2)

View File

@ -406,7 +406,7 @@ handleREPLEvent :: AppState -> BrickEvent Name AppEvent -> EventM Name (Next App
handleREPLEvent s (VtyEvent (V.EvKey (V.KChar 'c') [V.MCtrl])) =
continue $
s
& gameState . robotMap . ix "base" . machine %~ cancel
& gameState . robotMap . ix 0 . machine %~ cancel
handleREPLEvent s (VtyEvent (V.EvKey V.KEnter [])) =
if not $ s ^. gameState . replWorking
then case processTerm' topTypeCtx topCapCtx entry of
@ -419,12 +419,12 @@ handleREPLEvent s (VtyEvent (V.EvKey V.KEnter [])) =
else continueWithoutRedraw s
where
entry = formState (s ^. uiState . uiReplForm)
topTypeCtx = s ^. gameState . robotMap . ix "base" . robotContext . defTypes
topCapCtx = s ^. gameState . robotMap . ix "base" . robotContext . defCaps
topValCtx = s ^. gameState . robotMap . ix "base" . robotContext . defVals
topTypeCtx = s ^. gameState . robotMap . ix 0 . robotContext . defTypes
topCapCtx = s ^. gameState . robotMap . ix 0 . robotContext . defCaps
topValCtx = s ^. gameState . robotMap . ix 0 . robotContext . defVals
topStore =
fromMaybe emptyStore $
s ^? gameState . robotMap . at "base" . _Just . robotContext . defStore
s ^? gameState . robotMap . at 0 . _Just . robotContext . defStore
advanceREPL =
(uiState . uiReplForm %~ updateFormState "")
. (uiState . uiReplType .~ Nothing)
@ -432,8 +432,8 @@ handleREPLEvent s (VtyEvent (V.EvKey V.KEnter [])) =
. (uiState . uiError .~ Nothing)
startBaseProgram t@(ProcessedTerm _ (Module ty _) _ _) =
(gameState . replStatus .~ REPLWorking ty Nothing)
. (gameState . robotMap . ix "base" . machine .~ initMachine t topValCtx topStore)
. (gameState %~ execState (activateRobot "base"))
. (gameState . robotMap . ix 0 . machine .~ initMachine t topValCtx topStore)
. (gameState %~ execState (activateRobot 0))
handleREPLEvent s (VtyEvent (V.EvKey V.KUp [])) =
continue $ s & adjReplHistIndex Older
handleREPLEvent s (VtyEvent (V.EvKey V.KDown [])) =
@ -450,8 +450,8 @@ validateREPLForm s =
& uiState . uiReplForm %~ validate
& uiState . uiReplType .~ theType
where
topTypeCtx = s ^. gameState . robotMap . ix "base" . robotContext . defTypes
topCapCtx = s ^. gameState . robotMap . ix "base" . robotContext . defCaps
topTypeCtx = s ^. gameState . robotMap . ix 0 . robotContext . defTypes
topCapCtx = s ^. gameState . robotMap . ix 0 . robotContext . defCaps
result = processTerm' topTypeCtx topCapCtx (s ^. uiState . uiReplForm . to formState)
theType = case result of
Right (Just (ProcessedTerm _ (Module ty _) _ _)) -> Just ty
@ -505,7 +505,7 @@ handleWorldEvent s (VtyEvent (V.EvKey k []))
scrollView s (^+^ (worldScrollDist *^ keyToDir k)) >>= continue
handleWorldEvent s (VtyEvent (V.EvKey (V.KChar 'c') [])) = do
invalidateCacheEntry WorldCache
continue $ s & gameState . viewCenterRule .~ VCRobot "base"
continue $ s & gameState . viewCenterRule .~ VCRobot 0
-- pausing and stepping
handleWorldEvent s (VtyEvent (V.EvKey (V.KChar 'p') [])) = do
@ -599,15 +599,15 @@ makeEntity s e = do
mkPT = ProcessedTerm mkProg (Module mkTy empty) (S.singleton CMake) empty
topStore =
fromMaybe emptyStore $
s ^? gameState . robotMap . at "base" . _Just . robotContext . defStore
s ^? gameState . robotMap . at 0 . _Just . robotContext . defStore
case isActive <$> (s ^. gameState . robotMap . at "base") of
case isActive <$> (s ^. gameState . robotMap . at 0) of
Just False ->
continue $
s
& gameState . replStatus .~ REPLWorking mkTy Nothing
& gameState . robotMap . ix "base" . machine .~ initMachine mkPT empty topStore
& gameState %~ execState (activateRobot "base")
& gameState . robotMap . ix 0 . machine .~ initMachine mkPT empty topStore
& gameState %~ execState (activateRobot 0)
_ -> continueWithoutRedraw s
------------------------------------------------------------

View File

@ -42,6 +42,7 @@ import Control.Arrow ((&&&))
import Control.Lens
import Data.Array (range)
import qualified Data.Foldable as F
import qualified Data.IntMap as IM
import qualified Data.List as L
import Data.List.Split (chunksOf)
import qualified Data.Map as M
@ -249,7 +250,7 @@ drawMenu s =
where
isReplWorking = s ^. gameState . replWorking
isPaused = s ^. gameState . paused
viewingBase = (s ^. gameState . viewCenterRule) == VCRobot "base"
viewingBase = (s ^. gameState . viewCenterRule) == VCRobot 0
creative = s ^. gameState . creativeMode
gameModeWidget =
@ -316,7 +317,7 @@ drawWorld g =
robotsByLoc =
M.fromListWith (maxOn (^. robotDisplay . displayPriority)) . map (view robotLocation &&& id)
. M.elems
. IM.elems
$ g ^. robotMap
drawLoc :: W.Coords -> Widget Name
@ -591,7 +592,7 @@ drawREPL s =
debugging = False -- Turn ON to get extra line with history index
inputLines = 1 + fromEnum debugging
history = s ^. uiState . uiReplHistory
base = s ^. gameState . robotMap . at "base"
base = s ^. gameState . robotMap . at 0
histIdx = fromString $ show (history ^. replIndex)
fmt (REPLEntry e) = txt replPrompt <+> txt e
fmt (REPLOutput t) = txt t

View File

@ -22,7 +22,7 @@ tested-with: GHC ==8.10.4 || ==9.0.1
extra-source-files: CHANGELOG.md
example/*.sw
data-dir: data/
data-files: *.yaml, challenges/*.yaml
data-files: *.yaml, challenges/*.yaml, adjectives.txt, names.txt
source-repository head
type: git

View File

@ -101,6 +101,9 @@ parser =
, testCase
"Nested pair syntax"
(valid "(1,2,3,4)")
, testCase
"Binder at end of block"
(valid "r <- build {move}")
, testGroup
"failure location - #268"
[ testCase
@ -473,7 +476,7 @@ eval g =
evaluateCESK :: CESK -> IO (Either Text (Value, Int))
evaluateCESK cesk = flip evalStateT (g & creativeMode .~ True) . flip evalStateT r . runCESK 0 $ cesk
where
r = mkRobot "" zero zero cesk []
r = mkRobot (-1) Nothing "" [] zero zero defaultRobotDisplay cesk [] [] False
runCESK :: Int -> CESK -> StateT Robot (StateT GameState IO) (Either Text (Value, Int))
runCESK _ (Up exn _ []) = return (Left (formatExn exn))
@ -564,10 +567,17 @@ inventory =
testGroup
"Inventory"
[ testCase
"insert 0 / hash"
( assertEqual
"insertCount 0 x empty has same hash as x"
(x ^. E.entityHash)
(hash (E.insertCount 0 x E.empty))
)
, testCase
"insert / hash"
( assertEqual
"insert x empty has same hash as x"
(x ^. E.entityHash)
"insert x empty has same hash as 2*x"
(2 * (x ^. E.entityHash))
(hash (E.insert x E.empty))
)
, testCase
@ -587,15 +597,15 @@ inventory =
, testCase
"insert 2 / delete 3"
( assertEqual
"insert 2, delete 3 gives hash 0"
0
"insert 2, delete 3 gives hash of x"
(x ^. E.entityHash)
(hash (E.deleteCount 3 x (E.insertCount 2 x E.empty)))
)
, testCase
"deleteAll"
( assertEqual
"insert 2 x, insert 2 y, deleteAll x same hash as insert 2 y"
(hash (E.insertCount 2 y E.empty))
"insert 2 x, insert 2 y, deleteAll x same hash as insert 2 y, insertCount 0 x"
(hash (E.insertCount 0 x (E.insertCount 2 y E.empty)))
(hash (E.deleteAll x (E.insertCount 2 y (E.insertCount 2 x E.empty))))
)
, testCase