Skip to content

Commit c5660c7

Browse files
Jegors Čemisovssanderploegsma
Jegors Čemisovs
andauthored
Add MazeGenerator exercise and related tests (#2355)
* Add MazeGenerator exercise and related tests This commit introduces a new MazeGenerator class along with related Units Tests. It also adds the `Dimensions` record to represent the size of the maze. Both MazeGenerator and Dimensions classes are incorporated into the build.gradle file. For testing purposes, MazeGeneratorTest along with its configuration file and a test reference were incorporated. This change allows generating perfect mazes of a specified size with a single correct path, ideal for maze navigation tasks. Can produce random mazes or reproducible ones when provided with a seed. The mazes are represented using box-drawing characters and the entrances/exits are denoted by an arrow symbol(⇨). * Update exercises/practice/mazy-mice/src/main/java/MazeGenerator.java Co-authored-by: Sander Ploegsma <sanderploegsma@gmail.com> * Update exercises/practice/mazy-mice/src/main/java/MazeGenerator.java Co-authored-by: Sander Ploegsma <sanderploegsma@gmail.com> * Update mazy-mice exercise metadata configuration The config.json file for the mazy-mice exercise has been improved to include the new 'invalidator' field. This field references the build.gradle file which is used for the construction and testing of the 'MazeGenerator' example. With this addition, users are now able to work with the MazeGenerator class and its related unit tests in their local environment with ease. * Improve grammar in mazy-mice exercise instructions Minor grammatical correction has been made in the mazy-mice exercise documentation. Prior sentences were changed from "The small square maze" and "The rectangular maze" to "A small square maze" and "A rectangular maze" respectively, to improve sentence flow and clarity for users. * Update placeholder in hints.md The placeholder comment in the 'hints.md' file for the mazy-mice exercise has been replaced with the 'Hints' header. This change is essential for creating a more meaningful start to the document and improving the clarity for users trying to understand maze generation. * Refactor test method name for clarity The test method name 'the_dimensions_are_correct' in MazeGeneratorTest has been refactored to 'theDimensionsAreCorrect'. This is in line with Java's camelCase naming convention, improving readability and maintaining a consistent code style. * Update test method name to reflect logical assertion Updated the name of the test method in MazeGeneratorTest from 'twoMazesShouldNotBeEqual' to 'aMazeIsDifferentEachTimeItIsGenerated'. The new name better indicates the purpose of the test, which is to check that every maze generated is unique. * Add 'mazy-mice' to settings.gradle Included 'practice:mazy-mice' in the settings.gradle to extend the diversity of exercises. This step is necessary for building and running the 'mazy-mice' exercise. * Add new "mazy-mice" exercise to config.json A new exercise named "mazy-mice" has been added to config.json. This exercise, which involves character arrays and string handling with for-loops, is intended to diversify the exercise list and give users more exercise choices. The difficulty of this exercise has been set to 8. * Refactor maze generation and test methods for readability The methods for maze generation have been refactored to directly accept individual parameters for 'rows' and 'columns' instead of a 'Dimensions' object. This change simplifies the method calls and enhances code readability. Also, updated the corresponding test cases to adhere to the new method signature. Additionally, added enumeration for directions at the end of the test class to improve code organization and readability. * Add descriptions to auto-generated test cases An auto-generated 'tests.toml' file is introduced. The descriptions added provide information for each test key about the attributes and functionality that is being tested. These include correct dimensions, valid characters, proper locations of entrances and exits, and generation of mazes with or without a seed parameter. These descriptions help developers understand the purpose of the tests and correct functioning of the maze. Also, change method names and orders to make the test easier to follow. This allows the clearer reading of the tests, thus increasing productivity. It helps maintainers to spot and understand the new changes faster and easier. Minor changes, such as capitalisation corrections, are also made for improved readability. * Change access modifier for test methods in MazeGeneratorTest This commit changes the access modifier of all test methods in the `MazeGeneratorTest` class from default to public. This necessary change, switching to public, allows JUnit to access the test methods. Previously, due to default access level it could cause issues with the test runner and result in no tests being executed. Access modifiers were also applied to the `setup` method to ensure it properly annotated before each test run. Making these methods public improves test functionality and ensures they are recognized as legitimate JUnit tests. * Add additional hints for exercise in hints.md This commit adds important hints in the hints.md file for the 'mazy-mice' exercise. It introduces useful general information regarding the generation of random numbers in Java 17 including links to valuable resources. This could help participants to understand better how to deal with random numbers and thus can improve the exercise's learning objectives. * Add a new test for seeded maze generator This commit added a new test case to the MazeGeneratorTest file. The added test, "theMazeIsPerfectWithSeed", simulates a perfect maze generation with a predefined seed. This test aims to help to ensure that the maze generator can correctly use seed to produce the expected unique maze structure. This also enhances the test coverage and further ensures the robustness of the maze generating code. * Add tests for maze size validation In this commit, 4 new tests have been added to the MazeGeneratorTest in order to check the validity of size parameters passed to the 'generatePerfectMaze' function. These tests ensure that both rows and columns parameters are within acceptable range (5-100) by throwing an IllegalArgumentException if they are not. * Add maze dimensions validation to the MazeGenerator class Dimension validation methods have been included in the maze generation methods of the MazeGenerator class. This is to ensure that the rows and columns of the maze are within an acceptable range (5-100). If these values fall out of the given range, an IllegalArgumentException is thrown. This was introduced to prevent the generation of mazes that would be too small or too big, which could lead to performance issues or unplayable mazes. * Revise header formatting in instructions.md The heading formatting in the instructions.md document within the mazy-mice exercise directory has been revised. The markdown format was adjusted from using "##" to make it a sub-heading, to "#" to make it a top level heading. This change has been implemented for clarity and readability, making it easier for users to understand the structure of the document. * Remove redundant .meta/tests.toml file * delete version file * Remove Dimensions.java as its functionality is redundant The file Dimensions.java was deleted because its purpose - to represent the dimensions of a maze - is already covered by another class in our program. Its functionality including width and height calculations, which were previously done in cells, are now handled elsewhere, making this file unnecessary. * Update exercises/practice/mazy-mice/src/test/java/MazeGeneratorTest.java Co-authored-by: Sander Ploegsma <sanderploegsma@gmail.com> * Update exercises/practice/mazy-mice/src/test/java/MazeGeneratorTest.java Co-authored-by: Sander Ploegsma <sanderploegsma@gmail.com> * Update config.json to include new practices Added "strings" and "for-loops" to the "practices" array in the config.json file. This change is necessary to ensure all necessary learning concepts are being covered within the curriculum. * Add new game "Mazy Mice" in config.json The new game "Mazy Mice" with corresponding uuid, practices, prerequisites, and difficulty level was added to the config.json. This is essential for including a new game in the curriculum that emphasizes the application of "strings" and "for-loops" practices. * Add Dimensions.java to Mazy Mice example files Included "Dimensions.java" in the example files array for the "Mazy Mice" problem in config.json, as it is needed for the correct generation of the maze. Not including this could lead to errors or incomplete solutions. * Remove redundant mazeNotNull test in MazeGeneratorTest class The mazeIsNotNull test in MazeGeneratorTest class was removed as it was deemed unnecessary and redundant. The null check is already implicitly covered in the subsequent dimensions and valid characters tests. * Refactor MazeGeneratorTest for better test organization Moved `Direction` enum to the top of the MazeGeneratorTest class to improve code organization and readability. Adjusted dimensions test to use Arrays.stream for enhanced test flow and comprehension. Removed redundant mazeNotNull test. * Add status field to config.json --------- Co-authored-by: Sander Ploegsma <sanderploegsma@gmail.com>
1 parent 8425a9c commit c5660c7

File tree

11 files changed

+612
-0
lines changed

11 files changed

+612
-0
lines changed

config.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1983,6 +1983,23 @@
19831983
"numbers"
19841984
],
19851985
"difficulty": 3
1986+
},
1987+
{
1988+
"slug": "mazy-mice",
1989+
"name": "Mazy Mice",
1990+
"uuid": "1bac7473-9ee8-4cfc-928b-77792102ffc1",
1991+
"practices": [
1992+
"chars",
1993+
"strings",
1994+
"for-loops",
1995+
"arrays"
1996+
],
1997+
"prerequisites": [
1998+
"strings",
1999+
"for-loops"
2000+
],
2001+
"difficulty": 8,
2002+
"status": "beta"
19862003
}
19872004
]
19882005
},
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Hints
2+
3+
## General
4+
5+
- You can use the [Random class][random-class] to generate random numbers.
6+
- Read more in article: [Random Number Generators in Java 17][random-number-generators].
7+
8+
## Maze generation
9+
10+
You can use any algorithm to generate a perfect maze. The [recursive backtracker][recursive-backtracker] is a good choice.
11+
12+
## Box drawing characters
13+
14+
| Character | Name | Unicode |
15+
|:---------:|:--------------------------------------|:--------|
16+
|| box drawings light down and right | U+250C |
17+
|| box drawings light horizontal | U+2500 |
18+
|| box drawings light down and horizontal| U+252C |
19+
|| box drawings light down and left | U+2510 |
20+
|| box drawings light vertical | U+2502 |
21+
|| box drawings light up and right | U+2514 |
22+
|| box drawings light up and horizontal | U+2534 |
23+
|| box drawings light up and left | U+2518 |
24+
|| box drawings light vertical and right | U+2520 |
25+
|| rightwards white arrow | U+21E8 |
26+
27+
28+
[recursive-backtracker]: https://en.wikipedia.org/wiki/Maze_generation_algorithm
29+
[random-class]: https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Random.html
30+
[random-number-generators]: https://www.baeldung.com/java-17-random-number-generators
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Instructions
2+
3+
Your task is to generate the perfect mazes for Mickey and Minerva — those with only one solution and no isolated sections.
4+
Here's what you need to know:
5+
6+
- The maze has a rectangular shape with an opening at the start and end.
7+
- The maze has rooms and passages, which intersect at right angles.
8+
- The program should accept two parameters: rows and columns. The maze should be between 5 and 100 cells in size.
9+
- A maze which is `x` columns wide and `y` rows high should be `2x + 1` characters wide and `2y + 1` characters high.
10+
- If no seed is provided, generate a random maze. If the same seed is provided multiple times, the resulting maze should be the same each time.
11+
- Use [box-drawing][Box-drawing] characters to draw walls, and an arrow symbol (⇨) for the entrance on the left and exit on the right.
12+
13+
It's time to create some perfect mazes for these adventurous mice!
14+
15+
### Examples
16+
17+
1. A small square maze 5x5 cells (or 11x11 characters)
18+
```text
19+
┌───────┬─┐
20+
│ │ │
21+
│ ┌─┬── │ │
22+
│ │ │ │ ⇨
23+
│ │ │ ──┤ │
24+
⇨ │ │ │ │
25+
┌─┤ └── │ │
26+
│ │ │ │
27+
│ │ ────┘ │
28+
│ │
29+
└─────────┘
30+
```
31+
2. A rectangular maze 6x18 cells
32+
```text
33+
┌───────────┬─────────┬───────────┬─┐
34+
│ │ │ │ │
35+
│ ┌───────┐ │ ┌─┐ ──┐ └───┐ ┌───┐ │ │
36+
│ │ │ │ │ │ │ │ │ │ ⇨
37+
│ └─┐ ┌─┐ │ │ │ ├── ├───┐ │ │ ──┼── │
38+
│ │ │ │ │ │ │ │ │ │ │ │
39+
└── │ │ ├───┴───┤ ┌─┘ ┌─┘ │ ├── │ ──┤
40+
⇨ │ │ │ │ │ │ │ │ │
41+
┌─┬─┴─┐ └─┐ ┌─┐ │ └─┐ │ ┌─┘ │ ──┴─┐ │
42+
│ │ │ │ │ │ │ │ │ │ │ │
43+
│ │ │ └── │ │ │ └── │ ──┘ ┌─┘ ──┐ │ │
44+
│ │ │ │ │ │ │
45+
└───┴───────┴───────┴─────┴─────┴───┘
46+
```
47+
48+
[Box-drawing]: https://en.wikipedia.org/wiki/Box-drawing_character
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Introduction
2+
3+
Meet Mickey and Minerva, two clever mice who love to navigate their way through a maze to find cheese. They enjoy a good challenge, but with only their tiny mouse brains, they prefer if there is only one correct path to the cheese.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"authors": [
3+
"rabestro"
4+
],
5+
"files": {
6+
"solution": [
7+
"src/main/java/MazeGenerator.java"
8+
],
9+
"test": [
10+
"src/test/java/MazeGeneratorTest.java"
11+
],
12+
"example": [
13+
".meta/src/reference/java/MazeGenerator.java",
14+
".meta/src/reference/java/Dimensions.java"
15+
],
16+
"invalidator": [
17+
"build.gradle"
18+
]
19+
},
20+
"blurb": "Meet Mickey and Minerva, two clever mice who love to navigate their way through a maze to find cheese. They enjoy a good challenge, but with only their tiny mouse brains, they prefer if there is only one correct path to the cheese.",
21+
"source": "Inspired by the 'Maze Generator' created by Jan Boström at Alance AB.",
22+
"source_url": "https://mazegenerator.net/"
23+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Represents the dimensions of a maze.
3+
* <p>
4+
* Dimensions of a grid can be represented in cells or characters.
5+
* Rows and columns are used for cells, while width and height are used for characters.
6+
*/
7+
public record Dimensions(int rows, int columns) {
8+
/**
9+
* Returns the width of the maze in characters.
10+
*
11+
* @return the width of the maze
12+
*/
13+
int width() {
14+
return 2 * columns + 1;
15+
}
16+
17+
/**
18+
* Returns the height of the maze in characters.
19+
*
20+
* @return the height of the maze
21+
*/
22+
int height() {
23+
return 2 * rows + 1;
24+
}
25+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import java.util.Random;
2+
import java.util.random.RandomGenerator;
3+
import java.util.BitSet;
4+
import java.util.EnumSet;
5+
import java.util.Set;
6+
7+
import static java.util.stream.IntStream.range;
8+
9+
public class MazeGenerator {
10+
11+
public char[][] generatePerfectMaze(int rows, int columns) {
12+
validateDimensions(rows, columns);
13+
return new Grid(new Dimensions(rows, columns), RandomGenerator.getDefault())
14+
.generateMaze()
15+
.placeDoors()
16+
.print();
17+
}
18+
19+
public char[][] generatePerfectMaze(int rows, int columns, int seed) {
20+
validateDimensions(rows, columns);
21+
return new Grid(new Dimensions(rows, columns), new Random(seed))
22+
.generateMaze()
23+
.placeDoors()
24+
.print();
25+
}
26+
27+
private void validateDimensions(int rows, int columns) {
28+
if (rows < 5 || columns < 5 || rows > 100 || columns > 100) {
29+
throw new IllegalArgumentException("Dimensions must be in range.");
30+
}
31+
}
32+
}
33+
34+
enum Direction {
35+
NORTH(0, 1),
36+
EAST(1, 0),
37+
SOUTH(0, -1),
38+
WEST(-1, 0);
39+
private final int dx;
40+
private final int dy;
41+
42+
Direction(int dx, int dy) {
43+
this.dx = dx;
44+
this.dy = dy;
45+
}
46+
47+
public int dx() {
48+
return dx;
49+
}
50+
51+
public int dy() {
52+
return dy;
53+
}
54+
}
55+
56+
final class Grid {
57+
private final Dimensions dimensions;
58+
private final BitSet grid;
59+
private final RandomGenerator randomGenerator;
60+
61+
Grid(Dimensions dimensions, RandomGenerator randomGenerator) {
62+
this.dimensions = dimensions;
63+
this.grid = new BitSet(dimensions.width() * dimensions.height());
64+
this.randomGenerator = randomGenerator;
65+
}
66+
67+
Grid generateMaze() {
68+
generate(new Cell(1, 1));
69+
return this;
70+
}
71+
72+
private int random(int bound) {
73+
return randomGenerator.nextInt(bound);
74+
}
75+
76+
private Direction pickRandomDirection(Set<Direction> directions) {
77+
int size = directions.size();
78+
int itemIndex = random(size);
79+
var direction = directions.toArray(new Direction[size])[itemIndex];
80+
directions.remove(direction);
81+
return direction;
82+
}
83+
84+
Grid placeDoors() {
85+
new Cell(1 + 2 * random(dimensions.rows()), 0).erase();
86+
new Cell(1 + 2 * random(dimensions.rows()), dimensions.width() - 1).erase();
87+
return this;
88+
}
89+
90+
private void generate(Cell cell) {
91+
cell.erase();
92+
93+
var directions = EnumSet.allOf(Direction.class);
94+
do {
95+
var direction = pickRandomDirection(directions);
96+
var wall = cell.move(direction);
97+
var next = wall.move(direction);
98+
if (next.isValid() && next.isNotEmpty()) {
99+
wall.erase();
100+
generate(next);
101+
}
102+
} while (!directions.isEmpty());
103+
}
104+
105+
char[][] print() {
106+
return range(0, dimensions.height())
107+
.mapToObj(this::line)
108+
.toArray(char[][]::new);
109+
}
110+
111+
private char[] line(int x) {
112+
return range(0, dimensions.width())
113+
.map(y -> new Cell(x, y).symbol())
114+
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
115+
.toString()
116+
.toCharArray();
117+
}
118+
119+
private final class Cell {
120+
final int x;
121+
final int y;
122+
123+
private Cell(int x, int y) {
124+
this.x = x;
125+
this.y = y;
126+
}
127+
128+
boolean isValid() {
129+
return x > 0 && x < dimensions.height() && y > 0 && y < dimensions.width();
130+
}
131+
132+
void erase() {
133+
grid.set(index());
134+
}
135+
136+
boolean isNotEmpty() {
137+
return !isEmpty();
138+
}
139+
140+
boolean isEmpty() {
141+
return grid.get(index());
142+
}
143+
144+
int index() {
145+
return x * dimensions.width() + y;
146+
}
147+
148+
boolean isDoor() {
149+
return isEmpty() && (y == 0 || y == dimensions.width() - 1);
150+
}
151+
152+
Cell move(Direction direction) {
153+
return new Cell(x + direction.dx(), y + direction.dy());
154+
}
155+
156+
char symbol() {
157+
if (isDoor()) {
158+
return '⇨';
159+
}
160+
if (isEmpty()) {
161+
return ' ';
162+
}
163+
var n = x > 0 && new Cell(x - 1, y).isNotEmpty() ? 1 : 0;
164+
var e = y < dimensions.width() - 1 && new Cell(x, y + 1).isNotEmpty() ? 1 : 0;
165+
var s = x < dimensions.height() - 1 && new Cell(x + 1, y).isNotEmpty() ? 1 : 0;
166+
var w = y > 0 && new Cell(x, y - 1).isNotEmpty() ? 1 : 0;
167+
var i = n + 2 * e + 4 * s + 8 * w;
168+
return switch (i) {
169+
case 0 -> ' ';
170+
case 1, 5, 4 -> '│';
171+
case 2, 8, 10 -> '─';
172+
case 3 -> '└';
173+
case 6 -> '┌';
174+
case 7 -> '├';
175+
case 9 -> '┘';
176+
case 11 -> '┴';
177+
case 12 -> '┐';
178+
case 13 -> '┤';
179+
case 14 -> '┬';
180+
case 15 -> '┼';
181+
default -> throw new IllegalStateException("Unexpected value: " + i);
182+
};
183+
}
184+
}
185+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
apply plugin: "java"
2+
apply plugin: "eclipse"
3+
apply plugin: "idea"
4+
5+
// set default encoding to UTF-8
6+
compileJava.options.encoding = "UTF-8"
7+
compileTestJava.options.encoding = "UTF-8"
8+
9+
repositories {
10+
mavenCentral()
11+
}
12+
13+
dependencies {
14+
testImplementation "junit:junit:4.13"
15+
testImplementation "org.assertj:assertj-core:3.15.0"
16+
}
17+
18+
test {
19+
testLogging {
20+
exceptionFormat = 'full'
21+
showStandardStreams = true
22+
events = ["passed", "failed", "skipped"]
23+
}
24+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
public class MazeGenerator {
2+
3+
public char[][] generatePerfectMaze(int rows, int columns) {
4+
throw new UnsupportedOperationException("Delete this statement and write your own implementation.");
5+
}
6+
7+
public char[][] generatePerfectMaze(int rows, int columns, int seed) {
8+
throw new UnsupportedOperationException("Delete this statement and write your own implementation.");
9+
}
10+
}

0 commit comments

Comments
 (0)