Refactor
All checks were successful
/ maven-build (push) Successful in 20s

This commit is contained in:
Manuel Friedli 2026-03-07 23:47:38 +01:00
parent 824f038084
commit 3efb87bcd8
Signed by: manuel
GPG key ID: 41D08ABA75634DA1
24 changed files with 14 additions and 19 deletions

60
app/state/action.ts Normal file
View file

@ -0,0 +1,60 @@
import Maze from "../model/maze.ts";
export interface Action {
type: string,
[key: string]: boolean | number | string | object | null | undefined;
}
export const ID_ACTION_STARTED_LOADING = 'started_loading';
export function actionStartedLoading(): Action {
return {
type: ID_ACTION_STARTED_LOADING
}
}
export const ID_ACTION_LOADED_MAZE = 'loaded_maze';
export function actionLoadedMaze(maze: Maze): Action {
return {
type: ID_ACTION_LOADED_MAZE,
maze
}
}
export const ID_ACTION_LOADING_FAILED = 'loading_failed';
export function actionLoadingFailed(reason: string): Action {
return {
type: ID_ACTION_LOADING_FAILED,
reason
};
}
export const ID_ACTION_TOGGLED_SHOW_SOLUTION = 'toggled_show_solution';
export function actionToggledShowSolution(value: boolean): Action {
return {
type: ID_ACTION_TOGGLED_SHOW_SOLUTION,
value
}
}
export const ID_ACTION_CLOSED_MESSAGE_BANNER = 'closed_message_banner';
export function actionClosedMessageBanner(): Action {
return {
type: ID_ACTION_CLOSED_MESSAGE_BANNER
}
}
export const ID_ACTION_CLICKED_CELL = 'clicked_cell';
export function actionClickedCell(x: number, y: number): Action {
return {
type: ID_ACTION_CLICKED_CELL,
x,
y
}
}

59
app/state/reducer.ts Normal file
View file

@ -0,0 +1,59 @@
import handleUserClicked from "./userpathhandler.ts";
import {State} from "./state";
import {
Action,
ID_ACTION_CLICKED_CELL,
ID_ACTION_CLOSED_MESSAGE_BANNER,
ID_ACTION_LOADED_MAZE,
ID_ACTION_LOADING_FAILED,
ID_ACTION_STARTED_LOADING,
ID_ACTION_TOGGLED_SHOW_SOLUTION
} from "./action.ts";
import Maze from "../model/maze.ts";
export default function reduce(state: State, action: Action): State {
switch (action.type) {
case ID_ACTION_STARTED_LOADING: {
return {
...state,
maze: null,
loading: true,
errorMessage: null
}
}
case ID_ACTION_LOADED_MAZE: {
return {
...state,
loading: false,
maze: action.maze as Maze,
userPath: []
}
}
case ID_ACTION_LOADING_FAILED: {
return {
...state,
loading: false,
errorMessage: `Failed to load maze. Reason: ${action.reason}`
}
}
case ID_ACTION_TOGGLED_SHOW_SOLUTION: {
return {
...state,
showSolution: action.value as boolean
}
}
case ID_ACTION_CLOSED_MESSAGE_BANNER: {
return {
...state,
errorMessage: null
}
}
case ID_ACTION_CLICKED_CELL: {
// There's so much logic involved, externalize that into its own file.
return handleUserClicked(state, action.x as number, action.y as number);
}
default: {
throw new Error(`Unknown action: ${action.type}`);
}
}
}

18
app/state/state.ts Normal file
View file

@ -0,0 +1,18 @@
import Coordinates from "../model/coordinates.ts";
import Maze from "../model/maze.ts";
export interface State {
errorMessage: string | null,
loading: boolean,
maze: Maze | null,
showSolution: boolean,
userPath: Coordinates[]
}
export const INITIAL_STATE: State = {
errorMessage: null,
loading: false,
maze: null,
showSolution: false,
userPath: []
};

View file

@ -0,0 +1,82 @@
import {State} from "./state";
import Coordinates from "../model/coordinates.ts";
import {MazeCell} from "../model/maze.ts";
export default function handleUserClicked(state: State, x: number, y: number): State {
if (isClickAllowed(x, y, state)) {
const maze = state.maze!;
// Okay, we clicked a cell that's adjacent to the end of the userpath (or which IS the end of the userpath)
// and that's not blocked by a wall. Now let's see.
if (-1 === state.userPath.findIndex(step => step.x === x && step.y === y)) {
// The clicked cell is not yet part of the userpath --> add it.
// If it's the end tile, also show a congratulation message
const showMessage = x === maze.end.x && y === maze.end.y;
return {
...state,
userPath: [...state.userPath, {x, y}],
errorMessage: showMessage ? "Congratulations! You won!" : state.errorMessage
};
} else {
// The clicked cell IS part of the userpath. Is it the last cell of it?
const lastCoordsFromUserPath = getLastCoordsFromUserPath(state)!;
if (lastCoordsFromUserPath.x === x && lastCoordsFromUserPath.y === y) {
// Yes, it's the last cell of the userpath --> remove it.
return {
...state,
userPath: state.userPath.filter(step => step.x !== x || step.y !== y),
errorMessage: null
}
}
}
}
// Not allowed to toggle that cell. Don't apply any change to the state.
return state;
}
function isClickAllowed(x: number, y: number, state: State): boolean {
const lastCoordsFromUserPath = getLastCoordsFromUserPath(state);
if (!lastCoordsFromUserPath) {
// when nothing has been marked yet, we can only toggle the starting position
return x === state.maze!.start.x && y === state.maze!.start.y;
}
if (lastCoordsFromUserPath.x === x && lastCoordsFromUserPath.y === y) {
// toggling the last position in the path is always allowed
return true;
}
if (Math.abs(x - lastCoordsFromUserPath.x) + Math.abs(y - lastCoordsFromUserPath.y) !== 1) {
// It's not a neighbor. So it's not allowed.
return false;
}
const lastCell = getCellAt(lastCoordsFromUserPath, state);
if (x === lastCoordsFromUserPath.x + 1) {
// There must be no wall to the right of the last cell.
return !lastCell.right;
}
if (x === lastCoordsFromUserPath.x - 1) {
// There must be no wall to the left of the last cell.
return !lastCell.left;
}
if (y === lastCoordsFromUserPath.y + 1) {
// There must be no wall below the last cell.
return !lastCell.bottom;
}
if (y === lastCoordsFromUserPath.y - 1) {
// There must be no wall above the last cell.
return !lastCell.top;
}
return false;
}
function getCellAt(coords: Coordinates, state: State): MazeCell {
return state.maze!.grid[coords.y][coords.x];
}
function getLastCoordsFromUserPath(state: State): Coordinates | null {
if (state.userPath.length > 0) {
return state.userPath[state.userPath.length - 1];
}
return null;
}