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()); } });