This commit is contained in:
parent
824f038084
commit
3efb87bcd8
24 changed files with 14 additions and 19 deletions
60
app/state/action.ts
Normal file
60
app/state/action.ts
Normal 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
59
app/state/reducer.ts
Normal 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
18
app/state/state.ts
Normal 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: []
|
||||
};
|
||||
82
app/state/userpathhandler.ts
Normal file
82
app/state/userpathhandler.ts
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue