The point of this project is to provide an evaluator for machine learning algorithms searching the space of Cargo-Bot problems and their solutions.
Game author Rui Viana and TwoLivesLeft software recently released an excellent puzzle/programming game for iPad, called Cargo-Bot. I recommend it highly as a great little time-waster. More interesting: it's the first game written entirely on the iPad.
Yes, on the iPad. Codea is an all-iPad scripting and game design system. Very neat.
Meanwhile, I happen to be trying to write a book on Genetic Programming, and the chapter on Conway's Game of Life was just... well, kinda dry for a starter. When my wife showed me Cargo-Bot, I immediately saw how straightforward it would be to make it into a testbed for GP.
Not Rui's game software, I mean, but the puzzle itself. After all, you write code in a simple domain-specific language; your program is graded on how well it performs, how many tokens it uses, and whether it obeys the constraints.
So what I've done (with Rui's permission) is build this little emulator that implements Cargo-Bot's algorithm in Ruby.
Nothing fancy: no graphics, no special learning algorithms, nothing you couldn't do in a couple of hours yourself, having played his elegant but interesting little puzzle-game.
CargoBot scripts in this program are a single string. Tokens recognized by the CargoBot class include:
R
,L
move the claw right or left one positionR_any
,L_any
move the claw only if it has a box in itR_none
,L_none
move the claw only if it has no boxR_[color]
,L_[color]
move the claw only if it has a box whose identifier is[color]
(see below)claw
activates the claw; if it's empty, it tries to pick up a box; if it's holding something, it puts it downclaw_any
only if it has a boxclaw_none
only if it is emptyclaw_[color]
only if it is holding that[color]
boxcall[N]
jump execution of the script to subroutine[N]
, returning to the next register of the calling subroutine when donecall[N]_any
,call[N]_none
andcall[N]_[color]
you can probably figure these out by now :)
There is also a special parser-control token I have added to the script language:
prog_[N]
subsequent tokens are appended to subroutine[N]
. The initial subroutine is 1; if the parser is asked to append tokens to a subroutine whose number doesn't already exist, it is created, and all missing subroutines are created as well. Subsequent calls toprog_[N]
return the parser's attention to that subroutine.
For example, parsing the script R prog_4 L_red claw call3 prog_2 claw prog_1 R_any call2
would result in the following CargoBot program:
subroutine 1: [:R, :R-any, :call2]
subroutine 2: [:claw]
subroutine 3: []
subroutine 4: [:L_red, :claw, :call3]
Any other token not mentioned here will be placed in a subroutine, but ignored when interpreted.
In Cargo-bot (the puzzle game), a single claw is controlled by your Program so that it moves left and right among a discrete and finite set of positions, each located above a pile of boxes of various colors. The claw can lift one box at a time, shift it left or right, and place it on top of a stack at its current position.
In this CargoBot
representation, the available positions in the "dock" are represented as a simple Array, containing Arrays representing "stacks", containing boxes represented as Symbols naming their color. These are used for initial setup of a puzzle condition, internal representation of dynamic state, and storing the puzzle's goal.
For example, to set up a "factory" with six stacks, where stack 3 contains three blue and stack 4 two red boxes, you can use the Ruby code
[[], [], [:b, :b, :b], [:r, :r], [], []]
I'll refer to the left stack as "stack 1", and the left/first item on a stack as being on the "bottom" (following the Ruby idiom of Array#push
and Array#pop
affecting the rightmost items on Array objects).
The method CargoBot#activate
resets various counters (used for evaluating the performance of a program) and flags, then operates every simply:
- all the tokens in subroutine 1 are pushed onto the top of the execution stack
- until a termination condition is achieved (see below), one token is popped from the execution stack and interpreted; if the token is a
call[N]
token, then every token in subroutineN
is pushed immediately onto the execution stack; otherwise, the claw is moved and the state of the bot is updated accordingly
- If the execution stack is empty, the program terminates.
- Infinite loops are of course possible. A step counter limits the number of tokens interpreted (default=200).
- If the CargoBot's
stacks
of boxes match itsgoal
after any step, execution halts. - If the
fragile
flag is set, and the claw tries to leave the "box" at any point during execution, it breaks, and the program halts immediately. - If the
unstable
flag is set, and the claw tries to enter a position where a stack is higher than theheight_limit
, the stack topples and the program halts immediately.
As you may have noticed, there are a few extensions of the CargoBot
class compared with the implementation in Rui's game.
- The
fragile
flag determines whether the claw will crash into a wall, or merely "bump" and stay where it is. The instance variableCargoBot#crashes
records the number of crashes if the flag isn't set. - The
unstable
flag determines whether there's a height limit on stacks. If it's set, and the claw tries to enter a position where a stack is at (or over) theCargoBot#height_limit
, execution ends. Otherwise, the variableCargoBot#topples
records the number of times this constraint is violated. - There is no explicit limit on the number of subroutines or tokens in each one
- Any symbol can be used as a block 'color' except
:any
ornone
. Any filtered instructions do simple string-matching to trigger, so the tokenR_red
will only ever fire if the claw is holding a:red
block, not one labeled:r
- Infinite loops are cut off by
CargoBot#step_limit
- The running
CargoBot
instance records the number ofsteps
(tokens interpreted),moves
(L, R and claw moves only),crashes
,topples
- Any
CargoBot
created without a#goal
has a very unlikely goal set as a default; it probably won't ever end successfully
As far as I can tell, all the game's mechanics are captured here, with arguments and instance variables controlling the behavior and state of the bot's execution:
CargoBot.new("script")
is the basic call to create an instance, but it will have no blocks, and a very unlikely goal
.
Everything but the script itself can be accessed via hash arguments:
:stacks
should be set to the initial set of piles of boxes; it should be an Array of Arrays, containing symbols indicating the colors of boxes. The 'top' of a stack is considered to be the last element, as in Ruby's stack-handling Array methodsArray#pop
andArray#push
.:goal
should also be an Array of Arrays of Symbols. Note that if the boxes in thegoal
don't match thestacks
, your CargoBot might have a problem finishing the puzzle....:claw_position
is an Integer indicating which stack the claw is above (1-based!); default is 1:fragile
is a toggle which determines whether the claw breaks (crashing the CargoBot) when it tries to "leave the box"; default isfalse
:unstable
is a toggle which determines whether the machine breaks down when it the claw knocks over a stack of blocks that's too high; default isfalse
:height_limit
the maximum height a stack is allowed to get before toppling when the claw bumps it; default is 6:step_limit
termination condition to avoid loops; default is 200
I've included an evaluation method (in a separate class currently called CrateStacks
) which estimates the number of crates which have to be moved to change an observed arrangement of crates into a target arrangement. This is an intentionally unrealistic heuristic, but (apparently) one useful for search.
See http://github.com/Vaguery/CargoBot-ruby/tree/master/examples for some simple calls and demos (TBD). Run the acceptance test cucumber file to check to see that the Cargo-Bot tutorial examples are running.
I've also included a brief (and very kludgy) example that uses (multiobjective) hillclimbing to search for solutions to particular problems. Sometimes it works, sometimes it doesn't; don't treat it as anything but an experiment.
-
There may be an inconsistency with the original game's notion of height limits and how they work. In particular, whether a box can be placed "one above" maximum height, as long as the claw "cradles it" and doesn't move away. That is, we know the claw can topple an overheight stack if it approaches from the side; I don't know if there are conditions where a claw is over an overheight stack, and will topple it if it moves away from it without grabbing the top block....
-
It is possible to reach a goal when a box is still in the claw. As far as I can tell, this may be possible in the original game as well. So for instance you can make
[[:r, :y], [], []]
into[[], [:r], []]
and "win".