Loyola University Chicago — Department of Computer Science
Course: COMP 371 / COMP 471: Programming Languages (F26)
Instructor: Konstantin Läufer
Grade: Points (max 6.0)
A rescue robot has boarded a damaged space station. The interior structure is unstable, and rooms are unlit or collapsed. The robot must navigate through the corridors, collect crucial energy cells to recharge its failing battery, use an emergency flood-fill sonar scanner to map out connected chambers, and reach the escape pod before its power cell is completely drained!
###########
#R..#....E#
#.#.#.##..#
#.#...C#..#
#.#####.#.#
#...C...#.#
###########
R: Robot starting position (mutated to.once the game initializes)@: Robot's current live location on the rendered displayE: Escape pod exitC: Energy cell (+3 energy bonus when collected)#: Impassable wall.: Walkable open corridor / floor
In this project, you will:
- Transition from Java to Scala 3: Practice writing idiomatic Scala 3 syntax using significant indentation without unnecessary curly braces
{}. - Master Imperative & Procedural Programming: Model dynamic state using built-in Scala mutable data structures (
Array[Array[Char]],Array[Int], scalarvars, tuples, andOption) without domain-model classes, traits, or ADTs. - Analyze Parameter Passing & Aliasing: Deepen your understanding of call-by-value evaluation, reference copying, aliasing, and why parameter variables cannot be rebound to alter the caller's reference.
- Contrast Evaluation Strategies: Explore Scala's call-by-name evaluation (
=> Boolean) and see how it differs from call-by-value. - Implement Recursion & Trace Activation Records: Implement a multi-way recursive flood fill (
revealReachable), reason about base cases and termination guarantees, and draw call stack frames. - Apply Test-Driven Development (TDD): Work with JUnit 5 (Jupiter), interpret compiler contracts under strict typing (
-language:strictEquality,-Yexplicit-nulls,-Werror), and write targeted unit tests for boundary conditions. - Reflect on Architectural Paradigms: Experience firsthand why a purely procedural approach with loosely associated arrays and scalar variables becomes unwieldy as software scales—setting the stage for Object-Oriented design and Algebraic Data Types in Project 2.
.
├── build.sbt # Build definitions and compiler options
├── src
│ ├── main
│ │ ├── resources
│ │ │ └── mazes # External ASCII maze definition files
│ │ │ ├── default.txt # Standard 11×7 maze
│ │ │ ├── tiny.txt # Minimal 7×5 maze
│ │ │ ├── corridor.txt # Long corridor 15×5 maze
│ │ │ ├── chambers.txt # Multi-chamber 13×7 maze
│ │ │ ├── arena.txt # Open arena 16×5 maze
│ │ │ └── labyrinth.txt # Complex 17×8 maze
│ │ └── scala
│ │ └── mazesolver
│ │ └── MazeSolver.scala # Procedures & interactive CLI entry point
│ └── test
│ └── scala
│ └── mazesolver
│ └── MazeSolverTest.scala # JUnit 5 Jupiter test suite
└── README.md # Assignment specification & written report
The assignment is staged in four sequential parts:
In src/main/scala/mazesolver/MazeSolver.scala, implement the core imperative procedures:
-
isWalkable(maze: Array[Array[Char]], row: Int, col: Int): Boolean- Return
trueif(row, col)is within the grid boundaries and the character at that position is not a wall ('#').
- Return
-
directionDelta(direction: Char): Option[(Int, Int)]- Map movement commands (
'U','D','L','R') to coordinate offset deltas:-
'U'$\rightarrow$ (-1, 0) -
'D'$\rightarrow$ (1, 0) -
'L'$\rightarrow$ (0, -1) -
'R'$\rightarrow$ (0, 1) - Any other character
$\rightarrow$ None
-
- Map movement commands (
-
moveRobot(maze: Array[Array[Char]], robot: Array[Int], direction: Char): Boolean- Check if the destination cell in the specified direction is walkable.
- If walkable, mutate the
robotarray in place (robot(0) = newRow,robot(1) = newCol) and returntrue. - If blocked or invalid direction, leave
robotunchanged and returnfalse.
-
collectCell(maze: Array[Array[Char]], robot: Array[Int]): Int- If the robot is currently on an energy cell (
'C'), overwrite the grid cell with floor ('.') and return1. - Otherwise, return
0. (An energy cell can only be collected once!)
- If the robot is currently on an energy cell (
-
isAtExit(maze: Array[Array[Char]], robot: Array[Int]): Boolean- Return
trueif the robot's current position matches the exit marker ('E').
- Return
-
render(maze: Array[Array[Char]], robot: Array[Int], energy: Int, cellsCollected: Int): String- Format the 2-D maze grid into a multi-line string with the robot rendered as
'@'at its live position, followed by a status line:
s"Energy: $energy Cells: $cellsCollected"
- Format the 2-D maze grid into a multi-line string with the robot rendered as
-
playMoves(maze: Array[Array[Char]], robot: Array[Int], moves: String, startEnergy: Int): Int- Execute moves sequentially from
moves. - Each successful move costs 1 energy; collecting a cell awards +3 energy. Blocked moves cost 0 energy.
- Stop immediately if energy reaches 0 (stranded) or if the robot reaches the exit (
'E'). Return remaining energy.
- Execute moves sequentially from
-
playGame(maze: Array[Array[Char]], robot: Array[Int], moves: String, startEnergy: Int): (Int, Int, Boolean)- Execute moves tracking
(remainingEnergy, cellsCollected, reachedExit).
- Execute moves tracking
Complete and study the parameter-passing procedures:
moveNorth(position: Array[Int]): Unit- Decrement
position(0)by 1. Because Scala uses call-by-value with reference copying for arrays, the caller's array is mutated in place.
- Decrement
localReset(position: Array[Int]): Unit- Rebind a local
val replacement = Array(1, 1). Explain why assigning to a local variable does not alter which array the caller's variable references.
- Rebind a local
repeatUntilStopped(action: => Boolean): Int- Execute the call-by-name parameter
actionin a while-loop as long as it evaluates totrue. - Return the total count of successful attempts. Explain why call-by-name evaluates
actionanew on every iteration.
- Execute the call-by-name parameter
Implement the emergency flood-fill scanner:
-
revealReachable(maze: Array[Array[Char]], revealed: Array[Array[Boolean]], row: Int, col: Int): Int- Base cases:
-
(row, col)is out of maze bounds$\rightarrow$ return0 -
maze(row)(col) == '#'(wall)$\rightarrow$ return0 -
revealed(row)(col) == true(already visited)$\rightarrow$ return0
-
- Recursive step:
- Mark
revealed(row)(col) = true - Recursively explore the 4 cardinal neighbors: up, down, left, right.
- Return
1 + sum of newly revealed neighbors.
- Mark
-
Termination condition: Explain why setting
revealed(row)(col) = truebefore the recursive calls guarantees termination even in mazes with loops.
- Base cases:
Complete all sections in the Written Report Deliverables section at the bottom of this README.md.
- State-Transition Table: Trace 3 consecutive moves on the small test maze (
R,D,D). - Parameter-Passing Memory Diagram: Visual diagram showing stack frames and heap arrays for
moveNorthandlocalReset. - Scanner Call-Stack Trace: Detailed activation record trace for the 4×4 maze starting at
(1, 1). - Recursion vs. Iteration Analysis: Space complexity comparison (call stack vs heap stack).
- Procedural vs. OO Reflection: Analysis of why loosely bundled arrays and procedures become awkward and how ADTs / OO will solve this.
- Strictly Imperative / Procedural: Use only built-in types (
Array,String,Int,Boolean, tuples, andOption). Do NOT create custom classes, case classes, traits, or domain ADTs. - Syntax & Style: Use Scala 3 significant indentation (2 spaces, no unnecessary
{}). Code must passsbt scalafmtCheckAll. - Compiler Flags: Code must compile cleanly with
-language:strictEquality,-Yexplicit-nulls, and-Werror(warnings treated as errors). - Test Verification: All existing tests in
MazeSolverTest.scalamust pass. - Student Unit Tests: Add at least four original test cases to
StudentMazeSolverTestcovering edge cases.
Implement revealReachableIterative in MazeSolver.scala:
- Use an explicit mutable stack (
scala.collection.mutable.Stack[(Int, Int)]) on the heap to perform flood fill instead of recursive call stack frames. - In your written report, contrast heap allocation with JVM stack frame limits, detailing when and why an iterative stack avoids
StackOverflowError.
Open a terminal in the project directory:
# Compile the project
sbt compile
# Run the test suite
sbt test
# Check code formatting (reformat with 'sbt scalafmtAll')
sbt scalafmtCheckAll
# Run test coverage verification
sbt clean coverage test coverageReport
# Stage the application executable
sbt stage
# Run the interactive game with default maze
sbt run
# Run with a specific sample maze
sbt "run labyrinth.txt"
sbt "run chambers.txt"When running sbt run:
U: Move up (costs 1 energy)D: Move down (costs 1 energy)L: Move left (costs 1 energy)R: Move right (costs 1 energy)S: Emergency scanner flood fillH: Display help messageQ: Quit
- Ensure all your work is committed and pushed to your assigned GitHub Classroom repository.
- Verify that the GitHub Actions CI workflow runs and passes cleanly.
- Submit a brief inline note on Sakai with the URL of your repository before the deadline.
| Points | Criterion | Description |
|---|---|---|
| 1.0 | Navigation & Bounds | Correct implementation of isWalkable, directionDelta, and in-place moveRobot. |
| 0.5 | Game Mechanics & State | Correct implementation of collectCell, isAtExit, playMoves, and playGame. |
| 0.5 | Rendering | Correct implementation of render with robot @ overlay and formatted status line. |
| 0.5 | Parameter Passing Lab | Correct implementation of moveNorth, localReset, and memory aliasing demonstration. |
| 0.5 | Call-by-Name Evaluation | Correct implementation of repeatUntilStopped and explanation of re-evaluation. |
| 1.0 | Recursive Flood Fill | Correct implementation of revealReachable with base cases, 4-way recursion, and termination. |
| 0.5 | Memory Diagram & Stack Trace | Completed written deliverables: parameter passing memory diagram and scanner activation record trace. |
| 0.5 | State Table & OO Reflection | Completed written deliverables: state-transition table and procedural vs. OO architectural reflection. |
| 0.5 | Student Unit Tests | Thorough unit test cases added in StudentMazeSolverTest verifying edge cases. |
| 0.5 | Scala 3 Style & Strict Typing | Code conforms to -Yexplicit-nulls, -language:strictEquality, passes scalafmtCheckAll, and avoids custom domain classes. |
- -1.0 pt: Modification of existing procedure signatures, test fixtures, or project architecture.
- -1.0 pt: Introduction of custom domain classes, case classes, traits, or ADTs (violating the imperative project constraint); we will soon work on a fully-object oriented version of this project!
- -1.0 pt: Compiler warnings under
-Werroror unformatted code failingscalafmtCheckAll. - -1.0 pt: Missing AI transcript or affidavit that AI was not used.
Fill in your answers to each of the five sections below directly within this file.
Trace the three moves R, D, D on the smallLines 5×5 maze starting at (1, 1) with energy = 10 and cellsCollected = 0:
#####
#R.C#
#.#.#
#..E#
#####
| Step | Move Command | Attempted Pos | Walkable? | Updated Robot Pos | Remaining Energy | Cells Collected | Note |
|---|---|---|---|---|---|---|---|
| 0 | Init | — | — | (1, 1) |
10 | 0 | Initial state |
| 1 | R |
(1, 2) |
[TODO: Yes/No] | [TODO] | [TODO] | [TODO] | [TODO] |
| 2 | D |
(2, 2) |
[TODO: Yes/No] | [TODO] | [TODO] | [TODO] | [TODO] |
| 3 | D |
(2, 2) |
[TODO: Yes/No] | [TODO] | [TODO] | [TODO] | [TODO] |
Illustrate with an ASCII diagram the stack and heap layout during execution of:
moveNorth(pos)localReset(pos)
[TODO: Provide your ASCII memory diagram here showing Stack Frames, Callee Parameters, and Heap Arrays]
Written Explanation:
-
Why does
moveNorthmutate the caller's array, whereas passing anIntvariable (e.g.energy) cannot be mutated by the callee?[TODO: Your answer here]
-
Why does Scala reject
position = Array(1, 1)in parameter lists, and what happens when you rebind a localval replacement = Array(1, 1)?[TODO: Your answer here]
-
How does call-by-name (
action: => BooleaninrepeatUntilStopped) fundamentally differ from passing a reference or passing a primitive value?[TODO: Your answer here]
Given the 4×4 tinyLines maze:
####
#..#
#.##
####
Trace revealReachable(maze, revealed, 1, 1):
[TODO: Show the sequence of activation records pushed and popped from the call stack, including arguments (row, col) and return values]
Questions:
-
What values are stored in each activation record (stack frame) on the JVM?
[TODO: Your answer here]
-
Which invocations hit the base cases (out of bounds, wall
#, or already revealed)?[TODO: Your answer here]
-
Why is marking
revealed(row)(col) = trueprior to recursive neighbor exploration essential to avoid infinite recursion?[TODO: Your answer here]
-
What is the maximum theoretical stack depth in the worst-case maze of size
$M \times N$ ?[TODO: Your answer here]
Compare revealReachable (recursive) with revealReachableIterative (explicit heap stack):
- Memory location of state:
[TODO: Your answer here]
- Risk of
StackOverflowErrorvs.OutOfMemoryError:[TODO: Your answer here]
- Performance tradeoffs (function invocation overhead vs object allocation):
[TODO: Your answer here]
In this assignment, all state is represented using raw arrays (Array[Array[Char]], Array[Int]) and loose variables passed into top-level procedures.
-
What problems or vulnerabilities arise when multiple procedures directly read and mutate naked arrays?
[TODO: Your answer here]
-
How would an Object-Oriented or ADT-based design (e.g.,
Maze,Robot,Cell,Position) improve encapsulation, type safety, and maintainability in Project 2?[TODO: Your answer here]
- No AI tools were used on this assignment.
- AI tools were used (describe tool, prompts, and provide transcript link below or transcript document(s) in doc/ subdirectory):
[TODO: AI transcript link or statement]