Files
snake/src/redux/snake.js

226 lines
6.2 KiB
JavaScript

import Module from "./module.js";
import {
registerCaller,
unregisterCaller,
startLoop,
stopLoop,
subscribeKeyPressed,
unsubscribeKeyPressed
} from "./game.js";
const MODULE_NAME = "SNAKE";
const DEFAULT_GRID_SIZE = 16;
const initialState = {
grid: createGrid(DEFAULT_GRID_SIZE),
size: DEFAULT_GRID_SIZE,
paused: false,
vX: 1,
vY: 0,
snake: [],
score: 0,
previousScore: 0,
apple: randomPosition(DEFAULT_GRID_SIZE)
};
/* === Snake Helper Functions ================================================================== */
export const directions = {
UP: "UP",
DOWN: "DOWN",
LEFT: "LEFT",
RIGHT: "RIGHT"
};
export const keys = {
UP: "ArrowUp",
RIGHT: "ArrowRight",
DOWN: "ArrowDown",
LEFT: "ArrowLeft",
h: "h",
j: "j",
k: "k",
l: "l",
p: "p",
s: "s",
r: "r"
};
function randomPosition(size) {
return [Math.round(Math.random() * (size - 1)), Math.round(Math.random() * (size - 1))];
}
const comparePositions = (pos1, pos2) => pos1[0] === pos2[0] && pos1[1] === pos2[1];
function createGrid(size) {
const rows = [];
for (let y = 0; y < size; y += 1) {
const columns = [];
for (let x = 0; x < size; x += 1) {
if (Math.random() > 9) {
columns[x] = "x";
} else {
columns[x] = " ";
}
}
rows[y] = columns;
}
return rows;
}
const markCell = (grid, [x, y], mark) => {
grid[y][x] = mark;
return grid;
};
const moveSnakePart = ([x, y], vX, vY, size) => {
let newX = x + vX;
let newY = y + vY;
if (newX > size - 1) {
newX = 0;
}
if (newX < 0) {
newX = size - 1;
}
if (newY > size - 1) {
newY = 0;
}
if (newY < 0) {
newY = size - 1;
}
return [newX, newY];
};
export const module = new Module(MODULE_NAME, initialState);
/* === Actions ================================================================================== */
export const [START_SNAKE, startSnake] = module.action("START_SNAKE");
export const [RESET_SNAKE, resetSnake] = module.action("RESET_SNAKE", options => ({ ...options }));
export const [STOP_SNAKE, stopSnake] = module.action("STOP_SNAKE");
export const [PAUSE_SNAKE, pauseSnake] = module.action("PAUSE_SNAKE");
export const [UPDATE_FRAME_SNAKE, updateFrameSnake] = module.action("UPDATE_FRAME_SNAKE");
export const [CHANGE_DIRECTION, changeDirection] = module.action("CHANGE_DIRECTION", direction => ({ direction }));
export const [KEY_PRESSED_SNAKE, keyPressedSnake] = module.action("KEY_PRESSED_SNAKE", key => ({ key }));
export const [UPDATE_STATE, updateState] = module.action("UPDATE_STATE", (newState, fullUpdate = false) => ({
newState,
fullUpdate
}));
/* === Reducers ================================================================================= */
module.reducer(UPDATE_STATE, (state, { newState, fullUpdate }) => (fullUpdate ? newState : { ...state, ...newState }));
module.reducer(RESET_SNAKE, (state, options) => {
const grid = createGrid(state.size);
const { apple } = initialState;
const snake = options.started ? [[0, 0]] : [];
markCell(grid, apple, "a");
snake.forEach(p => markCell(grid, p, "s"));
return { ...state, ...initialState, grid, snake, ...options };
});
module.reducer(CHANGE_DIRECTION, (state, { direction }) => {
const { vX, vY } = state;
if (direction === directions.UP && vY !== 1) {
return { ...state, vX: 0, vY: -1 };
}
if (direction === directions.DOWN && vY !== -1) {
return { ...state, vX: 0, vY: 1 };
}
if (direction === directions.LEFT && vX !== 1) {
return { ...state, vX: -1, vY: 0 };
}
if (direction === directions.RIGHT && vX !== -1) {
return { ...state, vX: 1, vY: 0 };
}
return state;
});
module.reducer(UPDATE_FRAME_SNAKE, state => {
const { snake, vX, vY, size } = state;
let { apple, started, died, previousScore, score } = state;
const grid = createGrid(size);
const newSnake = [...snake];
const nextPosition = moveSnakePart(newSnake[newSnake.length - 1], vX, vY, size);
previousScore = score;
if (comparePositions(nextPosition, apple)) {
apple = randomPosition(size);
// eslint-disable-next-line no-loop-func
while (newSnake.filter(p => comparePositions(p, apple)).length) {
apple = randomPosition(size);
}
} else {
newSnake.shift();
}
if (newSnake.filter(p => comparePositions(p, nextPosition)).length) {
started = false;
died = true;
}
newSnake.push(nextPosition);
markCell(grid, apple, "a");
const brightness = (l, i) => 20 + Math.round(0.8 * ((i + 1) / l) * 100);
newSnake.forEach((p, i) => markCell(grid, p, "s" + brightness(newSnake.length, i)));
score = newSnake.length;
return { ...state, grid, vX, vY, snake: newSnake, apple, started, died, previousScore, score };
});
/* === Middleware =============================================================================== */
module.after(UPDATE_FRAME_SNAKE, (dispatch, { died }) => {
if (died) {
dispatch(unregisterCaller(UPDATE_FRAME_SNAKE));
dispatch(stopLoop());
}
});
module.middleware(START_SNAKE, (dispatch, { started }) => {
dispatch(resetSnake({ started: true, died: false }));
dispatch(registerCaller(UPDATE_FRAME_SNAKE));
dispatch(subscribeKeyPressed(KEY_PRESSED_SNAKE));
dispatch(startLoop());
});
module.middleware(STOP_SNAKE, (dispatch, { started }) => {
dispatch(unregisterCaller(UPDATE_FRAME_SNAKE));
dispatch(unsubscribeKeyPressed(KEY_PRESSED_SNAKE));
dispatch(stopLoop());
dispatch(resetSnake({ started: false }));
});
module.middleware(PAUSE_SNAKE, (dispatch, { paused }) => {
dispatch(updateState({ paused: !paused }));
if (paused) {
dispatch(registerCaller(UPDATE_FRAME_SNAKE));
} else {
dispatch(unregisterCaller(UPDATE_FRAME_SNAKE));
}
});
module.middleware(KEY_PRESSED_SNAKE, (dispatch, _, { key }) => {
if (key === keys.UP || key === keys.k) {
dispatch(changeDirection(directions.UP));
}
if (key === keys.DOWN || key === keys.j) {
dispatch(changeDirection(directions.DOWN));
}
if (key === keys.LEFT || key === keys.h) {
dispatch(changeDirection(directions.LEFT));
}
if (key === keys.RIGHT || key === keys.l) {
dispatch(changeDirection(directions.RIGHT));
}
if (key === keys.s) {
dispatch(stopSnake());
}
if (key === keys.p) {
dispatch(pauseSnake());
}
if (key === keys.r) {
dispatch(startSnake());
}
});