Hi Dear Reader!
In my earlier posts we dealt with:
- Time Complexity
- Space Complexity
- Cognitive Complexity
In this post we are going after a different kind of complexity:
- Cyclomatic Complexity
And we'll do OOP!
Are you thinking what I'm thinking?
Yeah :)
It's gonna OOP-pee pants city here real soon!
Beginning
You don't like classic Java, huh?
Well, you are out of luck:
- Dad bought us tickets for a classical Java Symphony.
- And we are going to go there.
- Like it or not.
- And yes, you are going to miss your dumb little Champions League match on TV, because of it.
Man up!
Dad's tickets
Dad gave us tickets to a classical Java Symphony.
It is a Sliding Puzzle.
There is a 3 x 3 grid, a black square, and some U-shapes:
The goal is to get RED inside BLUE.
By moving the black square and dragging stuff with it:
No. The Symphony is NOT about how to solve the puzzle manually!
The Symphony is about how to implement the puzzle as a game (and also to add a search algorithm on top of it)!
And the Symphony is in classical Java.
Going to the Symphony
While we are driving to the concert, I'm gonna read the Brochure Dad gave me.
Grid is 3 by 3, and you can move the square North, East, South, West:
You can drag other things which are around the square:
You can NOT enter a U-shape from one of its closed sides:
NO! We are not turning home for the match, we are going to the concert! Drive!
So...
The same applies to dragged shapes too. They will collide also:
Etiquette
We were given a problem, and we want to model it.
Officially it is called State Space Representation.
- Our world can be in a State at a specific point in time,
- Possible states form a State Space,
- To filter out nonsense states, we introduce Constraints,
- We start from Initial State,
- We are looking for Goal States (i.e. we must define the "is it good?" predicate),
- We get from state to state with Operators.
I made a checklist:
[ ] Find parking spot: Define State Space + Constraints
[ ] Walk in: Initial State
[ ] Coat check: Predicate for goal state
[ ] Find seats: Operators
Yeah we are really doing it! Yes, you are coming.
Find parking spot: Define State Space + Constraints
While you are parking the car, I'm gonna tell you what our State Space is:
What is our constraint? Only {0, 1, 2} are allowed out of ints.
We will make sure later in code that holds true, but we'll use ints.
So, our classical Java concert starts with Position:
public record Position(int row, int col) { }
But we need how many Positions?
Four! Corresponding to each shape.
How to model many Positions?
Array-array-array? No, can't query directly a shape:
Record? No, you can't iterate easily:
Hashmap? Pfff, buckets for 4 elements? Are you serious? No:
Good old array works fine:
We can check out the State Space:
[X] Find parking spot: Define State Space + Constraints
[ ] Walk in: Initial State
[ ] Coat check: Predicate for goal state
[ ] Find seats: Operators
Let's walk in!
Walk in: Initial State
Omg hurry, just open the door and get in:
[X] Find parking spot: Define State Space + Constraints
[X] Walk in: Initial State
[ ] Coat check: Predicate for goal state
[ ] Find seats: Operators
Hurry-hurry, coat check!
Coat check: Predicate for goal state
OMG, don't play with your phone! Can you forget about that stupid match for this evening p-l-e-a-s-e?
Now, define the predicate:
[X] Find parking spot: Define State Space + Constraints
[X] Walk in: Initial State
[X] Coat check: Predicate for goal state
[ ] Find seats: Operators
Find seats: Operators
Oh my God. What do you mean you don't know how the seat numbering works?
Argh... I can't believe you sometimes...
Okay. Let me explain.
There are invalid moves, so we want to make a predicate whether "Moving Down/South is valid?"
There are four directions:
public enum Direction {
UP(-1, 0),
RIGHT(0, 1),
DOWN(1, 0),
LEFT(0, -1);
private final int rowChange;
private final int colChange;
Direction(int rowChange, int colChange) {
this.rowChange = rowChange;
this.colChange = colChange;
}
...
So we're gonna branch:
public boolean isLegalMove(Direction direction) {
return switch (direction) {
case UP -> canMoveUp();
case RIGHT -> canMoveRight();
case DOWN -> canMoveDown();
case LEFT -> canMoveLeft();
};
}
Up is legal if:
private boolean canMoveUp() {
return getPosition(BLOCK).row() > 0 && isEmpty(getPosition(BLOCK).moveUp());
}
Left is legal if:
private boolean canMoveLeft() {
return getPosition(BLOCK).col() > 0 && isEmpty(getPosition(BLOCK).moveLeft());
}
Right is legal if:
private boolean canMoveRight() {
if (getPosition(BLOCK).col() == BOARD_SIZE - 1) {
return false;
}
final var right = getPosition(BLOCK).moveRight();
return isEmpty(right)
|| (getPosition(BLACK_SHOE).equals(right) && !haveSamePosition(BLOCK, BLUE_SHOE));
}
Down is legal if:
Hurry, please!!!!!
So, where was I? Oh... down move predicate, yes:
private boolean canMoveDown() {
if (getPosition(BLOCK).row() == BOARD_SIZE - 1) {
return false;
}
final var down = getPosition(BLOCK).moveDown();
if (isEmpty(down)) {
return true;
}
if (haveSamePosition(BLACK_SHOE, BLOCK)) {
return false;
}
return getPosition(BLUE_SHOE).equals(down)
|| (getPosition(RED_SHOE).equals(down) && !haveSamePosition(BLUE_SHOE, BLOCK));
}
OMG! NO! We are not done yet! These were just the predicates.
We still have to move things, you dum-dum!
Once again, branch:
public void makeMove(Direction direction) {
switch (direction) {
case UP -> moveUp();
case RIGHT -> moveRight();
case DOWN -> moveDown();
case LEFT -> moveLeft();
}
}
We can move up:
private void moveUp() {
if (haveSamePosition(BLACK_SHOE, BLOCK)) {
if (haveSamePosition(RED_SHOE, BLOCK)) {
movePiece(RED_SHOE, Direction.UP);
}
movePiece(BLACK_SHOE, Direction.UP);
}
movePiece(BLOCK, Direction.UP);
}
We can move right:
private void moveRight() {
movePieces(Direction.RIGHT, RED_SHOE, BLUE_SHOE, BLACK_SHOE);
}
We can move down:
private void moveDown() {
movePieces(Direction.DOWN, RED_SHOE, BLUE_SHOE, BLACK_SHOE);
}
We can move left:
private void moveLeft() {
movePieces(Direction.LEFT, RED_SHOE, BLUE_SHOE);
}
And that's it! Yay!
Now take a seat, you are ready for the symphony.
Waiting for the Symphony to begin
So are you excited? Mozart's Graph Quartet is going to begin soon! Real Graph Search!
What?!
Awh my Gawwwwd... you don't know what it is... are you serious?
I can't believe I'm marrying you in two months... omg...
Okay... we wasted so much time with finding seats that we basically have like 1 minute left, so I'll be quick:
But be careful of reentries (cycles), this is why it is a Graph search not a Tree search (acyclic):
The Breadth First Search... omg...
We really don't have time to explain it, essentially you have a Dequeue or something, plus a Set:
And you do the following:
Sorry, we have no more time... Just listen to the symphony and try enjoying it!
Summary
Now the classic Java Symphony begins!
Hmh...
Yes.
Sorry for the build up.
But I had to do it.
To make you feel the problem with the methods.
From now on, though we're gonna do fun things again.
The Match
Yeah. Those operators that Java cooked up were a bit too unpleasant.
Forget everything I've said earlier. No more classical Java for today!
Goal 1: User wants arbitrary amount of arbitrary sized and facing U-shapes.
Goal 2: Minimize LOC.
Goal 3: It has to be football themed.
We are going to break the entire puzzle down into digestible chunks.
But the overarching picture is:
- There's Your Half and Enemy Half
- You score goal by kicking balls from the Half Line into Enemy Half
- But you have to get to the Half Line from the middle of Your Half
What is a football field?
Your Half vs. Enemy Half. You always kick westward:
No edge cases:
It will make sense, I'll prove to you.
For now, focus on the goal: Goal.
How to kick the ball into the Enemy Half?
Scoring a goal is easy! Here it is:
What about the other cases?
There are NO other cases
All other cases collide:
So, it is simply:
Done.
If things don't make sense yet, they will in the next section.
How to kick the ball upto Half Line?
Now, here's the key: What is a Half?
What are U-shapes then?
And don't forget, who are YOU:
Now... we have to kick it:
Kicking is partitioning (we are trying to partition it into 2 subsets, sort of a graph component / transitive closure / rigid body problem):
So, what is the Go Set?
Now comes the One Billion Dollar Question:
And this is why I said earlier that it WILL have a westward side:
Now I hope you start feeling where this is headed :)
How to combine them in footbal?
So, how to combine these things into a Game of Football?
We start with some state:
You partition:
Then you score a goal:
Now comes the One Trillion Dollar question:
What happens when you collide / can't score a goal?
Remember all those possibilities? All those code branches?
On collision, you simply don't mutate the original state, leave it as it is:
The main thing is The Invariant:
- Before the algorithm, you have two Halves which are valid.
- After the algorithm, you have two Halves which are valid.
Now the game is clear, and we can make this into code.
Coding time, yay!
First, let's assume we are storing stuff in arbitrarily ordered arrays.
To look neat, I ordered them, but they are not necessarily in order:
You find "Pushed" the greatest pushed:
You min search collision:
Here's an example of the mapping:
Okay. How to move things? What things to move?
First find the "Puller", aka the greatest which has walls on both sides:
Then do a stupid for loop:
Now we can play the puzzle manually (and breadth first search in the background to suggest next best move):
Or even arbitrary puzzle with arbitrary pieces, like the one depicted here:
And now, the moment came for the codebase which does the business logic.
Here it is, in its full CS 101, simplified, step by step style:
-
diris the movement direction the Player picked -
source_xandsource_yis the position of the black square (i.e. the Player) -
state.piecesis an array storing all U-shapes - I purposefully broke it down into multiple loops (you can do it with 2 loops: a combined search, then a mutation pass)
use egui::Color32;
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
pub enum Dir { N, E, S, W }
#[derive(PartialEq, Eq, Clone, Hash)]
pub struct Piece { pub color: Color32, pub x: i8, pub y: i8, pub rank: i8, pub open: Dir }
#[derive(PartialEq, Eq, Clone, Hash)]
pub struct State { pub size: i8, pub x: i8, pub y: i8, pub pieces: Vec<Piece> }
pub fn move_mut(dir: &Dir, state: &mut State) -> bool {
let source_x = state.x;
let source_y= state.y;
let (target_x, target_y, opposite_dir) = match dir {
Dir::N => (source_x + 0, source_y - 1, Dir::S),
Dir::E => (source_x + 1, source_y + 0, Dir::W),
Dir::S => (source_x + 0, source_y + 1, Dir::N),
Dir::W => (source_x - 1, source_y + 0, Dir::E),
};
// 0. Boundary check, you must remain on the Grid
if target_x < 0 || target_x >= state.size || target_y < 0 || target_y >= state.size { return false; }
// 1. Find Greatest Pushed
let mut pushed = 0; // Black square is 0, has all walls, and is in source cell
for p in & state.pieces {
if p.x == source_x && p.y == source_y && p.open != *dir {
pushed = pushed.max(p.rank);
}
}
// 2.1. Find Smallest Hole
let mut hole = pushed + 1; // Must be vacuously true
for p in & state.pieces {
if p.x == target_x && p.y == target_y {
let rank = if p.open == opposite_dir { p.rank } else { 0 };
hole = hole.min(rank);
}
}
// 2.2. Collision check (just to make it explicit how dumb it is)
let fits = pushed < hole;
// 2.E Return without mutation
if !fits { return false; }
// 3.1. Find Greatest Puller
let mut puller = 0; // Black square is 0, has all walls, and is in source cell
for p in & state.pieces {
if p.x == source_x && p.y == source_y && p.open != *dir && p.open != opposite_dir {
puller = puller.max(p.rank);
}
}
// 3.2. Move all moved U-shapes
for p in &mut state.pieces {
if p.x == source_x && p.y == source_y && (p.open != *dir || p.rank < puller) {
p.x = target_x; p.y = target_y;
}
}
// 3.3. Move the Black Square too, remember: It is NOT an element of U-shapes!
state.x = target_x; state.y = target_y;
return true;
}
And of course, it works on arbitrary cases:
And that's it!
Futhermore there's even more order if you take a look at it from bird's eye view:
Which looks neat :) (i.e. further exploitable pattern in the structure)
Real Summary
Just with the Heineken fake event, unknowing football fans were persuaded into the concert by:
By using football, we got the following gains:
- We made it infinitely scalable (based on Natural Numbers).
- We replaced 388 LOC, 33 methods and some constructors/private fields/constants I didn't count.
The price we had to pay is that it is no longer directly representing business rules:
- Remember the first Java part?
- It was lengthy and complicated,
- but it directly encoded the literal rules (like you can move up if the target is empty and bla bla...)
All in all, I hope you found it interesting.
In summary:
- Java is a cool OO GC language.
- Rust is a cool C-in-spirit language, with some OO, FP paint job.
But to be fair... my eyes don't like Rust.
Doc understates it:
Many new users to Rust experience something we like to call ‘fighting with the borrow checker’, where the Rust compiler refuses to compile a program that the author thinks is valid.
That borrow checker doodad made my eyes hurt.
Yep Rust too has a rap... I mean a Rust whistle.
But objectively though, all languages are cool on its own, and they have their own way of handling problems.
Example: Java has this problem with records, that adding a field messes up the diff due to the ,:
| Original | Added c | Diff |
+----------+----------+----------+
{ {
a, a,
b b, change with ","
} c add "c"
}
How to tackle this problem?
The Way of Nature
Nature only wants to please itself. Get others to please it too. Likes to lord over them. To have its own way. It finds reasons to be unhappy when all the world is shining around it, and love is smiling through all things.
The Way of Nature is to change the world for the better:
Modify BNF to allow dangling commas.
| Original | Added c | Diff |
+----------+----------+----------+
{ {
a, a,
b, b,
} c, add "c,"
}
The Way of Grace
Grace doesn't try to please itself. Accepts being slighted, forgotten, disliked. Accepts insults and injuries.
The Way of Grace is to accept:
| Original | Added c | Diff |
+----------+----------+----------+
{ a { a
, b , b
} , c add ", c"
}
According to Terrence Malick, these are the two ways.
The Third Way
Of course Terrence Malick was not trying to make a point about Programming.
In Programming there's always the Third Way: My way of problem solving.
My...khm... solution to the puzzle laid out in this post is just a "solution".
I just arbitrarily decided on a recursively defined Set, with the base case of Black Square to make induction work.
There's an underlying structure though: Nat Set and slap on a 2D vector, plus add the collision relation.
A true solution would truly capture this, work it out in a more elegant form. So my "solution" is just a hack.
If you still not get what I'm poking at, then read this post. It outlines via a carnivale game, that certain problems can be reduced into really cool simple forms.
I hope that you'll come up with the true representation of the underlying structure, unlike my Third Way Solution did!






























































Top comments (0)