The Big Book of Small EK9 Programs
Adapted from Al Sweigart's The Big Book of Small Python Projects: 81 Easy Practice Programs (No Starch Press, 2021; copyright © 2021 Al Sweigart), which he makes freely available under a Creative Commons Attribution-NonCommercial-ShareAlike licence at inventwithpython.com/bigbookpython. Changes were made: each program was ported from Python to EK9 and remodelled in EK9's idioms.
Project 37 — Hungry Robots
This is project 37 of the book, ported to EK9. Its chapter is still being written; until then, this page shows the port itself: a run of it, the whole program, and how it keeps to the original.
A sample run
The program is random, so the run fixes it by setting BIGBOOK_SEED=1. The player's answers are piped in:
S
S
QUIT
Run it with:
BIGBOOK_SEED=1 ek9 hungry-robots.ek9 < input.txt
It prints:
Hungry Robots, by Al Sweigart al@inventwithpython.com
░░░░░░░░░░░░░░░░░░░░
░ R ░░ ░
░ ░ @░
░░ ░ ░R ░░
░ ░░ ░ ░░
░ ░ R ░ ░
░░ ░ ░ R X ░
░░ ░ ░ ░
░ X ░ R ░
░░░░░░░░░░░░░░░░░░░░
(T)eleports remaining: 2
(Q) (W) ( )
(A) (S) ( )
Enter move or QUIT: (Z) ( ) ( )
░░░░░░░░░░░░░░░░░░░░
░ ░░ ░
░ ░ R R@░
░░ ░ ░ ░░
░ ░░ ░R ░░
░ ░ R ░ ░
░░ ░ ░ X ░
░░ ░ ░R░
░ X ░ ░
░░░░░░░░░░░░░░░░░░░░
(T)eleports remaining: 2
(Q) (W) ( )
( ) (S) ( )
Enter move or QUIT: (Z) ( ) ( )
░░░░░░░░░░░░░░░░░░░░
░ ░░ ░
░ ░ R @░
░░ ░ R ░ ░░
░ ░░ ░ R ░░
░ ░ ░ ░
░░ ░ ░ XR░
░░ ░ ░ ░
░ X ░ ░
░░░░░░░░░░░░░░░░░░░░
You have been caught by a robot!
The program
#!ek9 defines module bigbook.hungryrobots defines constant Width <- 20 Height <- 10 RobotCount <- 5 TeleportCount <- 2 DeadRobotCount <- 2 WallCount <- 20 //The board's own marks, data like the move keys: they are drawn, not worded. WallMark <- "░" DeadRobotMark <- "X" PlayerMark <- "@" RobotMark <- "R" Blank <- " " //The keypad's first two rows are drawn three spaces in, as this port has always drawn them. KeypadIndent <- " " defines type //What the board itself holds. Robots and the player move every turn, so they are not //cells: they are positions drawn over the board. Cell Empty Wall DeadRobot //Every move the player can make, in the order the keypad lays them out. Parsing a reply //into this type is the validation: anything else is an unset Move. QUIT is not a move: it //is a word the player types, so the Console recognises it. Move NorthWest North NorthEast West Stay East SouthWest South SouthEast Teleport defines record //A board position, compared by value, so it can be a Dict key and be found in a List. //The same shape as lib/Coord.ek9. Coord x as Integer: Integer() y as Integer: Integer() default Coord() as pure //Integer(px) copies the argument: a stored key must not change when the caller's //Integer does. Coord() as pure -> px as Integer py as Integer x :=? Integer(px) y :=? Integer(py) default operator ? default operator <=> default operator #? defines class //THE input idiom of this book, copied unchanged from lib/LineInput.ek9 (the reasons are //there): read with next(), and every input loop also stops when ended() is true. LineInput stdin as Stdin: Stdin() inputEnded as Boolean: false next() <- rtn as String: stdin.next() if not rtn? inputEnded: true ended() as pure <- rtn as Boolean: Boolean(inputEnded) override operator ? as pure <- rtn as Boolean: true //THE INPUT AND OUTPUT: everything the player sees or types goes through here, worded by Say. Console stdout as Stdout: Stdout() input as LineInput: LineInput() say as Say: Say("en") Console() -> words as Say say: words showIntroduction() stdout.println(say.introduction()) //The board is drawn, not worded: computed from the game's cells, robots and player. showBoard() -> board as Dict of (Coord, Cell) robots as List of Coord player as Coord for y in 0 ... Height - 1 line <- "" for x in 0 ... Width - 1 line += glyphAt(board: board, robots: robots, player: player, at: Coord(x, y)) stdout.println(line) //Asks until the game allows the move; unset when the player QUITs or the input ends. nextMove() -> open as List of Move teleports as Integer <- rtn as Move: Move() quitting <- false while not rtn? and not quitting and not input.ended() showKeypad(open, teleports) if line <- input.next() reply <- line.upperCase().trim() if reply == say.quit() quitting: true else if move <- moveOf(reply) if isAllowed(open, move, teleports) rtn: move showSurvived() stdout.println(say.survived()) showCaught() stdout.println(say.caught()) showGoodbye() stdout.println(say.goodbye()) ended() as pure <- rtn as Boolean: input.ended() //The keys are the game's data; the keypad shows a key only for a direction the player //can move in. The prompt shares the keypad's bottom row. private showKeypad() -> open as List of Move teleports as Integer topRow <- keypadRow(open: open, left: Move.NorthWest, middle: Move.North, right: Move.NorthEast) bottomRow <- keypadRow(open: open, left: Move.SouthWest, middle: Move.South, right: Move.SouthEast) //Stay is always offered, so the middle row shows its key whatever is open. middleRow <- `(${offered(open, Move.West)}) (${keyOf(Move.Stay)}) (${offered(open, Move.East)})` stdout.println(say.teleportsLeft(teleports)) stdout.println(`${KeypadIndent}${topRow}`) stdout.println(`${KeypadIndent}${middleRow}`) stdout.println(`${say.askMove()} ${bottomRow}`) override operator ? as pure <- rtn as Boolean: true defines function //THE GAME: its rules, with no input or output. //Off the board reads as a wall, so no move or robot step needs a bounds check. cellAt() as pure -> board as Dict of (Coord, Cell) at as Coord <- rtn as Cell: board.getOrDefault(at, Cell.Wall) isOpen() as pure -> board as Dict of (Coord, Cell) robots as List of Coord at as Coord <- rtn as Boolean: cellAt(board, at) == Cell.Empty and not (robots contains at) //The key the player types for a move: the game's own data. keyOf() as pure -> move as Move <- rtn as String: String() switch move case Move.NorthWest rtn: "Q" case Move.North rtn: "W" case Move.NorthEast rtn: "E" case Move.West rtn: "A" case Move.Stay rtn: "S" case Move.East rtn: "D" case Move.SouthWest rtn: "Z" case Move.South rtn: "X" case Move.SouthEast rtn: "C" case Move.Teleport rtn: "T" default rtn: String() //The reverse of keyOf, derived from it, so the two cannot disagree. moveOf() as pure -> key as String <- rtn as Move: Move() for move in Move if keyOf(move) == key rtn: move //How far a move shifts the player along one axis: -1, 0 or +1. columnShift() as pure -> move as Move <- rtn as Integer: 0 if move == Move.NorthWest or move == Move.West or move == Move.SouthWest rtn: -1 else if move == Move.NorthEast or move == Move.East or move == Move.SouthEast rtn: 1 rowShift() as pure -> move as Move <- rtn as Integer: 0 if move == Move.NorthWest or move == Move.North or move == Move.NorthEast rtn: -1 else if move == Move.SouthWest or move == Move.South or move == Move.SouthEast rtn: 1 step() as pure -> from as Coord move as Move <- rtn as Coord: Coord(from.x + columnShift(move), from.y + rowShift(move)) compassMoves() as pure <- rtn as List of Move: [ Move.NorthWest, Move.North, Move.NorthEast, Move.West, Move.East, Move.SouthWest, Move.South, Move.SouthEast ] //The directions the player can move in from where they stand. openMoves() -> board as Dict of (Coord, Cell) robots as List of Coord player as Coord <- rtn as List of Move: List() of Move for direction in compassMoves() if isOpen(board, robots, step(player, direction)) rtn += direction //Stay is always allowed, Teleport while any are left, a direction only if open. isAllowed() as pure -> open as List of Move move as Move teleports as Integer <- rtn as Boolean: open contains move or move == Move.Stay if move == Move.Teleport rtn: teleports > 0 //One step towards a target along one axis: -1, 0 or +1. towards() as pure -> from as Integer target as Integer <- rtn as Integer: from < target <- 1 : from > target <- -1 : 0 //A robot heads straight for the player. Blocked by a wall, it tries the horizontal part of //the move, then the vertical part, and otherwise stays where it is. robotStep() as pure -> board as Dict of (Coord, Cell) robot as Coord player as Coord <- rtn as Coord: robot across <- Coord(robot.x + towards(robot.x, player.x), robot.y) upOrDown <- Coord(robot.x, robot.y + towards(robot.y, player.y)) direct <- Coord(across.x, upOrDown.y) if cellAt(board, direct) <> Cell.Wall rtn: direct else if cellAt(board, across) == Cell.Empty rtn: across else if cellAt(board, upOrDown) == Cell.Empty rtn: upOrDown randomEmptySpace() -> board as Dict of (Coord, Cell) robots as List of Coord rng as Random <- rtn as Coord: Coord() while not rtn? x <- rng.next(1, Width - 2) y <- rng.next(1, Height - 2) candidate <- Coord(x, y) if isOpen(board, robots, candidate) rtn: candidate newBoard() -> rng as Random <- rtn as Dict of (Coord, Cell): Dict() of (Coord, Cell) for x in 0 ... Width - 1 for y in 0 ... Height - 1 edge <- x == 0 or x == Width - 1 or y == 0 or y == Height - 1 rtn += DictEntry(Coord(x, y), edge <- Cell.Wall : Cell.Empty) noRobots <- List() of Coord for wall in 1 ... WallCount rtn += DictEntry(randomEmptySpace(rtn, noRobots, rng), Cell.Wall) for wreck in 1 ... DeadRobotCount rtn += DictEntry(randomEmptySpace(rtn, noRobots, rng), Cell.DeadRobot) addRobots() -> board as Dict of (Coord, Cell) rng as Random <- rtn as List of Coord: List() of Coord for robot in 1 ... RobotCount rtn += randomEmptySpace(board, rtn, rng) //Returns the robots still running, and marks each new crash site on the board in place. //A robot that is on, or steps onto, a dead robot is destroyed; two robots that step onto //the same space are both destroyed and leave a dead robot there. moveRobots() -> board as Dict of (Coord, Cell) robots as List of Coord player as Coord <- rtn as List of Coord: List() of Coord for robot in robots target <- robotStep(board, robot, player) crashed <- cellAt(board, robot) == Cell.DeadRobot or cellAt(board, target) == Cell.DeadRobot if not crashed if rtn contains target board += DictEntry(target, Cell.DeadRobot) rtn -= target else rtn += target defines function //The Console's drawing: computed output, with no words in it. glyphOf() as pure -> cell as Cell <- rtn as String? //The compiler asks for every Cell and a default (for an unset Cell), so rtn needs no initial value. switch cell case Cell.Wall rtn: WallMark case Cell.DeadRobot rtn: DeadRobotMark case Cell.Empty rtn: Blank default rtn: Blank //Walls and dead robots are drawn over everything, as in the original. glyphAt() as pure -> board as Dict of (Coord, Cell) robots as List of Coord player as Coord at as Coord <- rtn as String: glyphOf(cellAt(board, at)) if cellAt(board, at) == Cell.Empty rtn: at == player <- PlayerMark : robots contains at <- RobotMark : rtn //A key shown on the keypad, or a blank for a direction the player cannot move in. offered() as pure -> open as List of Move move as Move <- rtn as String: open contains move <- keyOf(move) : Blank keypadRow() as pure -> open as List of Move left as Move middle as Move right as Move <- rtn as String: `(${offered(open, left)}) (${offered(open, middle)}) (${offered(open, right)})` //THE source of randomness, copied unchanged from lib/RandomSource.ek9: SystemRandom(), or //SeededRandom(seed) when BIGBOOK_SEED holds a number (run-and-diff.sh sets it from seed.txt). randomSource() <- rtn as Random: SeededRandom(Integer(EnvVars().get("BIGBOOK_SEED"))) ?: SystemRandom() //THE language of the words, copied unchanged from lib/Language.ek9: BIGBOOK_LANG when it //is set (fr, zh, ...), otherwise en. language() <- rtn as String: EnvVars().get("BIGBOOK_LANG") ?: "en" //THE WORDS: every word the player sees or types. The board and the keypad are not here: the //Console draws them from the game's cells and keys. defines text for "en" Say introduction() "Hungry Robots, by Al Sweigart al@inventwithpython.com" teleportsLeft() -> teleports as Integer `(T)eleports remaining: ${teleports}` askMove() "Enter move or QUIT:" quit() "QUIT" goodbye() "Thanks for playing!" survived() "All the robots have crashed. You survived. Good job!" caught() "You have been caught by a robot!" defines program //Hungry Robots (Big Book project 37). Escape the robots by tricking them into crashing into each //other or into dead robots. The game decides; the Console asks and tells, in the player's //words. The golden replays a seeded game (seed.txt). HungryRobots() //A language with no words of its own is unset: ?: falls back to English. console <- Console(Say(language()) ?: Say("en")) rng <- randomSource() console.showIntroduction() board <- newBoard(rng) robots <- addRobots(board, rng) player <- randomEmptySpace(board, robots, rng) //A copy, because -= changes the Integer in place and must not change the constant. teleports <- Integer(TeleportCount) running <- true while running and not console.ended() console.showBoard(board, robots, player) if robots.empty() console.showSurvived() running: false else if move <- console.nextMove(openMoves(board, robots, player), teleports) if move == Move.Teleport teleports -= 1 player: randomEmptySpace(board, robots, rng) else player: step(player, move) robots: moveRobots(board, robots, player) if robots contains player console.showBoard(board, robots, player) console.showCaught() running: false else console.showGoodbye() running: false
About this port
- Faithful to the book: robots (R) move straight toward the player (@) each turn and are destroyed by crashing into each other or into dead robots (X); a robot blocked by walls (░) tries the horizontal part of its move, then the vertical part, and otherwise stays put; 8-direction moves (QWEDCXAZ), S to stay, T to teleport (limited), QUIT; win when all robots crash, lose if caught.
- Divergences: a 20 by 10 board (the book's WIDTH/HEIGHT/counts are "try changing" constants) for a readable golden; the "Press Enter" pause is omitted. A reply such as
QWis now re-prompted; the earlier port tested the reply as a substring of the available keys, so it acceptedQWand left the player where they were. - Model:
- The board is a
Dict of (Coord, Cell).Coordis a record compared by value (thelib/Coord.ek9shape);Cellis an enumeration (Empty,Wall,DeadRobot). Reading off the board withgetOrDefault(at, Cell.Wall)treats everything outside it as wall, so no move needs a bounds check. - Robots and the player are
Coords drawn over the board, not cells. The robots are aList of Coordrather than aSet: they move one at a time and a robot's fate can depend on where earlier robots went, so their order is part of the game. Moveis an enumeration of every move the player can make (QUIT is a word, not a move).keyOfgives a move's key andmoveOfparses a reply by searchingMovewithkeyOf, so the two cannot disagree; an unrecognised reply is an unsetMove, and the prompt loop guards on it withif move <- moveOf(...).- The rules are
as purefunctions (robotStep,step,isOpen,isAllowed), with no input or output in them.moveRobotsreturns the surviving robots and records new crash sites on the board it is given.
- The board is a