Refactored redux code and snake game

This commit is contained in:
2019-08-26 18:46:19 +02:00
parent fe7fae2c19
commit 918e5fee45
14 changed files with 1251 additions and 196 deletions

View File

@@ -1,3 +1,4 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": ["@babel/plugin-proposal-private-methods", "@babel/plugin-proposal-class-properties"]
}

View File

@@ -14,6 +14,8 @@
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-private-methods": "^7.4.4",
"@babel/preset-env": "^7.5.5",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.6",

View File

@@ -0,0 +1,96 @@
import React from "react";
import { Provider } from "react-redux";
import { hot } from "react-hot-loader/root";
import { createStore, combineReducers, compose, applyMiddleware } from "redux";
import { createBrowserHistory } from "history";
import { connectRouter, routerMiddleware, ConnectedRouter } from "connected-react-router";
import { reducers as keyboardReducers, middleware as keyboardMiddleware } from "../redux/keyboards.js";
import { middleware as apiMiddleware } from "../redux/api.js";
import { reducers as uiReducers } from "../redux/ui.js";
import { module as snakeModule } from "../redux/snake.js";
// import { reducers as gameReducers, middleware as gameMiddleware } from "../redux/game.js";
import { module as gameModule } from "../redux/game.js";
import { module as homeModule } from "../redux/home.js";
import { ThemeProvider } from "styled-components";
import { themes } from "../theming/theme.js";
import GlobalStyle from "../theming/GlobalStyle.js";
window.module = gameModule;
const history = createBrowserHistory({
basename: process.env.NODE_ENV === "production" ? "/redux/" : "/"
});
const store = createStore(
combineReducers({
keyboards: keyboardReducers,
ui: uiReducers,
router: connectRouter(history),
...snakeModule.reducers,
...gameModule.reducers,
...homeModule.reducers
}),
{},
compose.apply(
this,
[
applyMiddleware(
routerMiddleware(history),
apiMiddleware,
keyboardMiddleware,
snakeModule.middlewares,
gameModule.middlewares,
homeModule.middlewares
),
process.env.NODE_ENV !== "production" &&
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
].filter(m => m)
)
);
window.st = store;
const Document = ({ children }) => (
<Provider store={store}>
<ThemeProvider theme={themes.main}>
<ConnectedRouter history={history}>
<GlobalStyle />
{children}
</ConnectedRouter>
</ThemeProvider>
</Provider>
);
console.log("env", process.env.NODE_ENV);
process.env.NODE_ENV === "development" ? console.log("development") : console.log("production");
export default (process.env.NODE_ENV === "development" ? hot(Document) : Document);
// export default Document;
if (module.hot) {
module.hot.accept(
[
"../redux/snake.js",
"./Snake.js",
"./App.js",
"./Document.js",
"../redux/game.js",
"../redux/module.js",
"../redux/home.js"
],
() => {
store.replaceReducer(
combineReducers({
keyboards: keyboardReducers,
ui: uiReducers,
router: connectRouter(history),
...snakeModule.reducers,
...gameModule.reducers,
...homeModule.reducers
})
);
}
);
}

View File

@@ -1,6 +1,53 @@
import React from "react";
import Title from "../components/Title.js";
import Button from "../components/Button.js";
import Link from "../components/Link.js";
const Home = () => <Title large>Home Page</Title>;
import { connect } from "react-redux";
import { module as game, startLoop, stopLoop, registerCaller, unregisterCaller, setFps } from "../redux/game.js";
import { module as home, INCREMENT_COUNTER, resetCounter } from "../redux/home.js";
export default Home;
const Home = ({
startLoop,
stopLoop,
loopId,
registerCaller,
counter,
unregisterCaller,
setFps,
fps,
callers,
resetCounter
}) => (
<React.Fragment>
<Title large>Home Page</Title>
<p>
<b>fps:</b> {fps}
</p>
<p>
<b>loopId:</b> {loopId}
</p>
<p>
<b>counter:</b> {counter} <Link onClick={resetCounter}>Reset</Link>
</p>
<p>
<b>callers:</b> {callers}
</p>
<Button onClick={startLoop}>Start</Button>
<Button onClick={stopLoop}>Stop</Button>
<Button onClick={() => registerCaller(INCREMENT_COUNTER)}>Register</Button>
<Button onClick={() => unregisterCaller(INCREMENT_COUNTER)}>Unregister</Button>
<Button onClick={() => setFps(fps - 1)}>Slower</Button>
<Button onClick={() => setFps(fps + 1)}>Faster</Button>
</React.Fragment>
);
export default connect(
state => ({
loopId: state[game.name].loopId,
fps: state[game.name].fps,
callers: state[game.name].callers,
counter: state[home.name] && state[home.name].counter
}),
{ startLoop, stopLoop, registerCaller, unregisterCaller, setFps, resetCounter }
)(Home);

View File

@@ -1,7 +1,16 @@
import React from "react";
import { connect } from "react-redux";
import styled from "styled-components";
import { startSnake, stopSnake, pauseSnake, updateFrameSnake, keyPressedSnake, keys } from "../redux/snake.js";
import styled, { keyframes, css } from "styled-components";
import { module as gameModule, keyPress } from "../redux/game.js";
import {
module as snakeModule,
startSnake,
stopSnake,
pauseSnake,
updateFrameSnake,
keyPressedSnake,
keys
} from "../redux/snake.js";
import Title from "../components/Title.js";
import Button from "../components/Button.js";
@@ -18,24 +27,209 @@ const Cell = styled.div`
display: inline-block;
vertical-align: top;
text-align: center;
background-color: ${props => props.value && props.theme.snake.cellColors[props.value]};
background-color: ${props => props.theme.snake.cellColors[" "]};
box-shadow: 1px 1px 4px gray inset, -1px -1px 4px #fff inset;
`;
const Grid = ({ data }) =>
const SnakePart = styled.div`
width: ${({ theme }) => theme.snake.cell.size};
height: ${({ theme }) => theme.snake.cell.size};
background-color: ${props => props.value && `hsl(340, ${props.value.slice(1)}%, 65%)`};
box-shadow: 1px 1px 2px #ddd inset, -1px -1px 2px gray inset;
`;
const Apple = styled.div.attrs({ role: "img", "aria-label": "apple" })`
width: ${({ theme }) => theme.snake.cell.size};
height: ${({ theme }) => theme.snake.cell.size};
display: inline-block;
font-size: 1.3rem;
vertical-align: top;
line-height: 1.8rem;
padding-left: 0.0625rem;
&:after {
content: "${props => (!props.died ? "🍎" : "")}";
display: inline-block;
}
`;
const Skull = styled.div.attrs({ role: "img", "aria-label": "skull" })`
width: ${({ theme }) => theme.snake.cell.size};
height: ${({ theme }) => theme.snake.cell.size};
display: inline-block;
font-size: 1.3rem;
vertical-align: top;
line-height: 1.8rem;
background-color: ${props => props.value && `hsl(0, 0%, ${100 - props.value.slice(1)}% )`};
padding-left: 0.0625rem;
box-shadow: 1px 1px 2px #ddd inset, -1px -1px 2px gray inset;
&:after {
content: "💀";
display: inline-block;
}
`;
const flipIn = keyframes`
50% {
transform: rotateX(90deg);
}
100% {
transform: rotateX(0deg);
}
`;
const flipOut = keyframes`
0 {
transform: rotateX(0deg);
}
50% {
transform: rotateX(90deg);
}
`;
const Card = styled.div`
display: inline-block;
position: relative;
margin-right: 0.2em;
width: 1rem;
height: 2rem;
// perspective: -1rem;
&:before {
content: "${props => props.oldValue}";
position: absolute;
display: inline-block;
// display: none;
border: 1px solid gray;
width: 1rem;
height: 2rem;
left: 0;
top: 0;
text-align: center;
line-height: 2rem;
vertical-align: middle;
background-color: brown;
color: white;
transition: transform ease-out 100ms;
// animation: ${flipIn} 1s infinite;
${props =>
props.value !== props.oldValue &&
css`
animation: ${flipIn} 1s forward;
`};
}
&:after {
content: "${props => props.value}";
position: absolute;
display: inline-block;
border: 1px solid gray;
width: 1rem;
height: 2rem;
left: 0;
top: 0;
text-align: center;
line-height: 2rem;
vertical-align: middle;
background-color: brown;
color: white;
transition: transform ease-out 100ms;
// animation: ${flipOut} 1s infinite;
${props =>
props.value !== props.oldValue &&
css`
animation: ${flipOut} 1s forward;
`};
}
`;
const Grid = ({ data, died }) =>
data.map((r, y) => (
<Row key={`y${y}`}>
{r.map((c, x) => (
<Cell key={`x${x}y${y}`} value={c}>
{c === "a" && (
<span role="img" aria-label="apple">
🍏
</span>
)}
<Cell key={`x${x}y${y}`} died={died}>
{c === "a" && <Apple died={died} />}
{!died && c[0] === "s" && <SnakePart value={c} />}
{died && c[0] === "s" && <Skull value={c} />}
</Cell>
))}
</Row>
));
const Score = ({ score, previousScore }) => {
const now = Date.now();
const [lastChange, changeLastChange] = React.useState({ timestamp: Date.now(), score, previousScore });
if (score === lastChange.score) {
const scoreCards = Array.from(`${score}`.padStart(3, " "));
return (
<div className="scoreboard">
{scoreCards.map((score, index) => (
<Card key={`${index}-${score}`} value={score} oldValue={score} />
))}
</div>
);
}
if (score !== lastChange.score && now - lastChange.timestamp > 1200) {
const scoreCards = Array.from(`${score}`.padStart(3, " "));
const previousScoreCards = Array.from(`${lastChange.score}`.padStart(3, " "));
console.log("scoreCards, previousScoreCards", scoreCards, previousScoreCards);
return (
<div className="scoreboard">
{scoreCards.map((score, index) => (
<Card key={`${index}-${score}`} value={score} oldValue={previousScoreCards[index]} />
))}
</div>
);
}
if (score !== lastChange.score && now - lastChange.timestamp > 1200) {
changeLastChange({ timestamp: now, score, previousScore: lastChange.score });
}
const scoreCards = Array.from(`${lastChange.previousScore}`.padStart(3, " "));
// console.log("now - lastChange.timestamp", now - lastChange.timestamp);
if (score !== lastChange.previousScore) {
const newScoreCards = Array.from(`${score}`.padStart(3, " "));
console.log("updating");
return (
<div className="scoreboard">
{newScoreCards.map(
(score, index) =>
console.log(score, scoreCards[index]) || (
<Card key={`${index}-${score}`} value={score} oldValue={scoreCards[index]} />
)
)}
</div>
);
}
return (
<div className="scoreboard">
{scoreCards.map((score, index) => (
<Card key={`${index}-${score}`} value={score} oldValue={score} />
))}
</div>
);
// const scoreCards = Array.from(`${score}`.padStart(3, " "));
// const previousScoreCards = Array.from(`${lastScore}`.padStart(3, " "));
// if (lastScore < score - 1) {
// updateScore(score);
// }
// console.log("score, lastScore", score, lastScore);
// return (
// <div className="scoreboard">
// {scoreCards.map((score, index) => (
// <Card key={`${index}-${score}`} value={score} oldValue={previousScoreCards[index]} />
// ))}
// </div>
// );
};
// const ArrowButton = styled(Button)`
// min-width: 5rem;
// `;
@@ -58,25 +252,36 @@ class Snake extends React.Component {
super(props);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.supportedKeys = Object.values(keys);
window.me = this;
this.state = { previousPoints: 0 };
}
handleKeyDown(e) {
if (!this.supportedKeys.includes(e.key)) {
return;
}
this.props.keyPressedSnake(e.key);
this.props.keyPress(e.key);
}
componentDidMount() {
window.addEventListener("keydown", this.handleKeyDown, false);
this.props.startSnake();
!this.props.started && this.props.startSnake();
}
render() {
const { startSnake, stopSnake, pauseSnake, updateFrameSnake, snakeGame } = this.props;
const { grid, started, paused, gameId, snake, died, fps } = snakeGame;
const {
startSnake,
stopSnake,
pauseSnake,
updateFrameSnake,
grid,
started,
paused,
snake,
died,
fps,
score,
previousScore
} = this.props;
if (died) {
if (false && died) {
return (
<React.Fragment>
<Title large>Snake</Title>
@@ -96,6 +301,7 @@ class Snake extends React.Component {
return (
<React.Fragment>
<Title large>Snake</Title>
<Grid data={grid} />
<Button onClick={startSnake}>Start</Button>
</React.Fragment>
);
@@ -103,9 +309,9 @@ class Snake extends React.Component {
return (
<React.Fragment>
<Title large>Snake: Attempt {gameId}</Title>
<p>Points: {snake.length}</p>
<Grid data={grid} />
<Title large>Snake</Title>
<Score score={score} previousScore={previousScore} />
<Grid data={grid} died={died} />
<Button onClick={updateFrameSnake}>Next</Button>
<ToggleButton toggle={paused} onClick={pauseSnake}>
{paused ? "Resume" : "Pause"}
@@ -122,14 +328,18 @@ class Snake extends React.Component {
}
}
const mapStateToProps = ({ snakeGame }) => ({ snakeGame: snakeGame || {} });
const mapStateToProps = state => ({
...state[gameModule.name],
...state[snakeModule.name]
});
const mapActionsToProps = {
startSnake,
stopSnake,
pauseSnake,
updateFrameSnake,
keyPressedSnake
keyPressedSnake,
keyPress
};
export default connect(

19
src/helpers/GameLoop.js Normal file
View File

@@ -0,0 +1,19 @@
export default class GameLoop {
-_isRunning = false;
-_handler = null;
constructor(handler) {
this._handler = handler;
}
get isRunning() {
return this._isRunning;
}
start() {
this._isRunning = true;
}
stop() {
this._isRunning = false;
}
}

View File

@@ -1,52 +1,12 @@
import React from "react";
import ReactDOM from "react-dom";
import { createStore, combineReducers, compose, applyMiddleware } from "redux";
import { createBrowserHistory } from "history";
import { Provider } from "react-redux";
import { connectRouter, routerMiddleware, ConnectedRouter } from "connected-react-router";
import { reducers as keyboardReducers, middleware as keyboardMiddleware } from "./redux/keyboards.js";
import { middleware as apiMiddleware } from "./redux/api.js";
import { reducers as uiReducers } from "./redux/ui.js";
import { reducers as snakeReducers, middleware as snakeMiddleware } from "./redux/snake.js";
import { ThemeProvider } from "styled-components";
import { themes } from "./theming/theme.js";
import GlobalStyle from "./theming/GlobalStyle.js";
import Document from "./containers/Document.js";
import App from "./containers/App.js";
const history = createBrowserHistory({
basename: process.env.NODE_ENV === "production" ? "/redux/" : "/"
});
const store = createStore(
combineReducers({
keyboards: keyboardReducers,
ui: uiReducers,
snakeGame: snakeReducers,
router: connectRouter(history)
}),
{},
compose.apply(
this,
[
applyMiddleware(routerMiddleware(history), apiMiddleware, keyboardMiddleware, snakeMiddleware),
process.env.NODE_ENV !== "production" &&
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
].filter(m => m)
)
);
ReactDOM.render(
<Provider store={store}>
<ThemeProvider theme={themes.main}>
<ConnectedRouter history={history}>
<GlobalStyle />
<Document>
<App />
</ConnectedRouter>
</ThemeProvider>
</Provider>,
</Document>,
document.getElementById("root")
);

44
src/redux/game.backup.js Normal file
View File

@@ -0,0 +1,44 @@
const MODULE_NAME = "GAME";
const initialState = {
started: false,
handlers: []
};
const GAME_UPDATE_LOOP_STATE = "GAME_UPDATE_LOOP_STATE";
const GAME_START_LOOP = "GAME_START_LOOP";
const GAME_STOP_LOOP = "GAME_STOP_LOOP";
const GAME_REGISTER_LOOP_HANDLER = "GAME_REGISTER_LOOP_HANDLER";
const GAME_UNREGISTER_LOOP_HANDLER = "GAME_UNREGISTER_LOOP_HANDLER";
export const gameUpdateLoopState = started => ({ type: GAME_UPDATE_LOOP_STATE, started });
export const gameStartLoop = () => ({ type: GAME_START_LOOP });
export const gameStopLoop = () => ({ type: GAME_STOP_LOOP });
export const gameRegisterLoopHandler = handler => ({ type: GAME_REGISTER_LOOP_HANDLER, handler });
export const gameUnregisterLoopHandler = handler => ({ type: GAME_UNREGISTER_LOOP_HANDLER, handler });
export const reducers = (state = initialState, action) => {
if (action.type === GAME_UPDATE_LOOP_STATE) {
return { ...state, started: action.started };
}
if (action.type === GAME_REGISTER_LOOP_HANDLER) {
return { ...state, handlers: [...state.handlers, action.handler] };
}
if (action.type === GAME_UNREGISTER_LOOP_HANDLER) {
return { ...state, handlers: state.handlers.filter(handler => handler !== action.handler) };
}
return state;
};
export const middleware = ({ dispatch, getState }) => next => action => {
next(action);
if (action.type === GAME_START_LOOP) {
if (getState()[MODULE_NAME].started) dispatch(gameUpdateLoopState(true));
}
if (action.type === GAME_STOP_LOOP) {
dispatch(gameUpdateLoopState(false));
}
};
reducers.module = middleware.module = MODULE_NAME;

152
src/redux/game.js Normal file
View File

@@ -0,0 +1,152 @@
import Module from "./module.js";
export const MODULE_NAME = "GAME";
export const module = new Module(MODULE_NAME, {
loopId: null,
callers: [],
keyPressSubscribers: [],
fps: 5
});
export const keys = {
UP: "ArrowUp",
RIGHT: "ArrowRight",
DOWN: "ArrowDown",
LEFT: "ArrowLeft",
PLUS: "+",
MINUS: "-"
};
export const [START_LOOP, startLoop] = module.action("START_LOOP");
export const [STOP_LOOP, stopLoop] = module.action("STOP_LOOP");
export const [LOOP, loop] = module.action("LOOP", ({ recur, skip } = {}) => ({ recur, skip }));
export const [UPDATE_LOOP_ID, updateLoopId] = module.action("UPDATE_LOOP_ID", loopId => ({ loopId }));
export const [SET_FPS, setFps] = module.action("SET_FPS", fps => ({ fps }));
export const [INVOKE_CALLERS, invokeCallers] = module.action("INVOKE_CALLERS");
export const [REGISTER_CALLER, registerCaller] = module.action("REGISTER_CALLER", name => ({ name }));
export const [UNREGISTER_CALLER, unregisterCaller] = module.action("UNREGISTER_CALLER", name => ({ name }));
export const [KEY_PRESS, keyPress] = module.action("KEY_PRESS", key => ({ key }));
export const [SUBSCRIBE_KEY_PRESSED, subscribeKeyPressed] = module.action("SUBSCRIBE_KEY_PRESSED", name => ({ name }));
export const [UNSUBSCRIBE_KEY_PRESSED, unsubscribeKeyPressed] = module.action("UNSUBSCRIBE_KEY_PRESSED", name => ({
name
}));
module.reducer(UPDATE_LOOP_ID, (state, { loopId }) => {
return { ...state, loopId };
});
module.reducer(SET_FPS, (state, { fps }) => {
return { ...state, fps };
});
module.reducer(REGISTER_CALLER, (state, { name }) => {
if (!name) {
throw new Error("Argument 'name' is required.");
}
if (typeof name !== "string") {
throw new Error("Argument 'name' is must be a function.");
}
if (state.callers.includes(name)) {
return state;
}
return { ...state, callers: [...state.callers, name] };
});
module.reducer(UNREGISTER_CALLER, (state, { name }) => {
if (!name) {
throw new Error("Argument 'name' is required.");
}
if (typeof name !== "string") {
throw new Error("Argument 'name' is must be a function.");
}
if (!state.callers.includes(name)) {
return state;
}
return { ...state, callers: state.callers.filter(caller => caller !== name) };
});
module.reducer(SUBSCRIBE_KEY_PRESSED, (state, { name }) => {
if (!name) {
throw new Error("Argument 'name' is required.");
}
if (typeof name !== "string") {
throw new Error("Argument 'name' is must be a function.");
}
if (state.keyPressSubscribers.includes(name)) {
return state;
}
return { ...state, keyPressSubscribers: [...state.keyPressSubscribers, name] };
});
module.reducer(UNSUBSCRIBE_KEY_PRESSED, (state, { name }) => {
if (!name) {
throw new Error("Argument 'name' is required.");
}
if (typeof name !== "string") {
throw new Error("Argument 'name' is must be a function.");
}
if (!state.keyPressSubscribers.includes(name)) {
return state;
}
return { ...state, keyPressSubscribers: state.keyPressSubscribers.filter(subscriber => subscriber !== name) };
});
module.middleware(KEY_PRESS, (dispatch, { keyPressSubscribers = [], fps }, { key }) => {
keyPressSubscribers.forEach(subscriber => dispatch({ type: subscriber, key }));
if (key === keys.PLUS) {
dispatch(setFps(fps + 1));
}
if (key === keys.MINUS) {
dispatch(setFps(fps - 1));
}
});
module.middleware(INVOKE_CALLERS, (dispatch, { callers = [] }) => {
callers.forEach(caller => dispatch({ type: caller }));
});
module.middleware(START_LOOP, (dispatch, { loopId, callers, fps }) => {
if (loopId) {
return;
}
dispatch(loop());
});
module.middleware(LOOP, (dispatch, { loopId, fps }, { skip = 0, recur = false }, getState) => {
if (loopId) {
return;
}
let start = null;
const loop = ({ recur = false, skip = 0 } = {}) => {
const { loopId, fps } = getState()[MODULE_NAME];
if (recur && !loopId) {
return null;
}
return requestAnimationFrame(timestamp => {
const minDuration = 1000 / fps;
if (!start) {
start = timestamp;
}
const progress = timestamp - start;
if (progress === 0 || progress > minDuration) {
start = timestamp;
dispatch(invokeCallers());
}
return loop({ recur: true, skip: skip > 10 ? 0 : skip + 1 });
});
};
dispatch(updateLoopId(loop()));
});
module.middleware(STOP_LOOP, (dispatch, { loopId }) => {
if (!loopId) {
return;
}
dispatch(updateLoopId(null));
});
export default module;

20
src/redux/home.js Normal file
View File

@@ -0,0 +1,20 @@
import Module from "./module.js";
const MODULE_NAME = "HOME";
export const module = new Module(MODULE_NAME, {
counter: 0
});
export const [INCREMENT_COUNTER, incrementCounter] = module.action("INCREMENT_COUNTER");
export const [RESET_COUNTER, resetCounter] = module.action("RESET_COUNTER");
module.reducer(INCREMENT_COUNTER, state => {
return { ...state, counter: state.counter + 1 };
});
module.reducer(RESET_COUNTER, state => {
return { ...state, counter: 0 };
});
export default module;

244
src/redux/module.js Normal file
View File

@@ -0,0 +1,244 @@
export class Middleware {
#action = null;
#middleware = null;
constructor(action, middleware) {
if (!action) {
throw new Error("Argument 'action' is required.");
}
if (typeof action !== "string") {
throw new Error("Argument 'action' must be a string.");
}
if (!middleware) {
throw new Error("Argument 'middleware' is required.");
}
if (typeof middleware !== "function") {
throw new Error("Argument 'middleware' must be a string.");
}
this.#action = action;
this.#middleware = middleware;
}
get action() {
return this.#action;
}
get middleware() {
return this.#middleware;
}
invoke(dispatch, state, action, getState) {
return this.#middleware(dispatch, state, action, getState);
}
}
export class After {
#action = null;
#handler = null;
constructor(action, handler) {
if (!action) {
throw new Error("Argument 'action' is required.");
}
if (typeof action !== "string") {
throw new Error("Argument 'action' must be a string.");
}
if (!handler) {
throw new Error("Argument 'handler' is required.");
}
if (typeof handler !== "function") {
throw new Error("Argument 'handler' must be a string.");
}
this.#action = action;
this.#handler = handler;
}
get action() {
return this.#action;
}
get handler() {
return this.#handler;
}
invoke(dispatch, state, action, getState) {
return this.#handler(dispatch, state, action, getState);
}
}
export class Reducer {
#action = null;
#reducer = null;
constructor(action, reducer) {
if (!action) {
throw new Error("Argument 'action' is required.");
}
if (typeof action !== "string") {
throw new Error("Argument 'action' must be a string.");
}
if (!reducer) {
throw new Error("Argument 'reducer' is required.");
}
if (typeof reducer !== "function") {
throw new Error("Argument 'reducer' must be a string.");
}
this.#action = action;
this.#reducer = reducer;
}
get action() {
return this.#action;
}
get reducer() {
return this.#reducer;
}
invoke(state, action) {
return this.#reducer(state, action);
}
}
export class Module {
#name = null;
#initialState = null;
#actions = {};
#after = {};
constructor(name, initialState) {
if (!name) {
throw new Error("Argument 'name' is required.");
}
this.#name = name;
this.#initialState = initialState;
this.middlewares = this.middlewares.bind(this);
}
get name() {
return this.#name;
}
middleware(action, middleware) {
this.#register(new Middleware(action, middleware));
}
reducer(action, reducer) {
this.#register(new Reducer(action, reducer));
}
after(action, middleware) {
this.#register(new After(action, middleware));
}
#register(reducerOrMiddleware) {
if (
!(reducerOrMiddleware instanceof Reducer) &&
!(reducerOrMiddleware instanceof Middleware) &&
!(reducerOrMiddleware instanceof After)
) {
throw new Error("Only objects of type Reducer, Middleware or After can register");
}
if (!(reducerOrMiddleware instanceof After)) {
if (this.#actions[reducerOrMiddleware.action]) {
throw new Error(`Reducer or middleware with action "${reducerOrMiddleware.action}" is already registered`);
}
this.#actions[reducerOrMiddleware.action] = reducerOrMiddleware;
} else {
this.#after[reducerOrMiddleware.action] = reducerOrMiddleware;
}
return reducerOrMiddleware;
}
action(name, action = () => {}) {
if (!name) {
throw new Error("Argument 'name' is required.");
}
if (typeof action !== "function") {
throw new Error("Argument 'action' must be function");
}
const type = `[${this.#name}] ${name}`;
const typedAction = (...args) => ({ ...action.apply(this, args), type });
return [type, typedAction];
}
#getActionByType(Type) {
return Object.keys(this.#actions)
.map(key => ({ key, action: this.#actions[key] }))
.reduce((actions, { key, action }) => (action instanceof Type ? { ...actions, [key]: action } : actions), {});
}
middlewares({ dispatch, getState }) {
return next => action => {
next(action);
const middlewares = this.#getActionByType(Middleware);
const middleware = middlewares[action.type];
if (middleware) {
middleware.invoke(dispatch, getState()[this.#name], action, getState);
}
const after = this.#after[action.type];
if (after) {
after.invoke(dispatch, getState()[this.#name], action, getState);
}
return null;
};
}
get reducers() {
return {
[this.#name]: (state = this.#initialState, action) => {
const reducers = this.#getActionByType(Reducer);
const reducer = reducers[action.type];
if (reducer) {
return reducer.invoke(state, action);
}
return state;
}
};
}
state(selector) {
return state => {
if (!this.#name) {
throw new Error("Unable to detect module name");
}
if (!state) {
throw new Error("No state was passed to the state selector");
}
if (!state[this.#name]) {
throw new Error(`There is no state for module '${this.#name}'`);
}
return selector(state[this.#name]);
};
}
select(...items) {
return (items || []).reduce((selection, item) => {
console.log("item", item);
return selection;
}, {});
}
}
export default Module;
export const attach = (modules, Component) => {
console.log("modules, Component", modules, Component);
return (...args) => {
console.log("args", args);
const component = Component({
GAME: {
actions: {
startLoop() {
console.log("=== START LOOP ===");
}
},
state: {}
}
});
return component;
component.props = { ...component.props };
console.log("component", component);
return component.render.apply(this, args);
};
};

215
src/redux/snake.backup.js Normal file
View File

@@ -0,0 +1,215 @@
const START_SNAKE = "[Snake] START_SNAKE";
const STOP_SNAKE = "[Snake] STOP_SNAKE";
const PAUSE_SNAKE = "[Snake] PAUSE_SNAKE";
const RESET_SNAKE = "[Snake] RESET_SNAKE";
const NEXT_FRAME_SNAKE = "[Snake] NEXT_FRAME_SNAKE";
const UPDATE_FRAME_SNAKE = "[Snake] UPDATE_FRAME_SNAKE";
const KEY_PRESSED_SNAKE = "[Snake] KEY_PRESSED_SNAKE";
const START_ANIMATION_SNAKE = "[Snake] START_ANIMATION_SNAKE";
const STOP_ANIMATION_SNAKE = "[Snake] STOP_ANIMATION_SNAKE";
export const startSnake = () => ({ type: START_SNAKE });
export const stopSnake = () => ({ type: STOP_SNAKE });
export const pauseSnake = () => ({ type: PAUSE_SNAKE });
export const resetSnake = () => ({ type: RESET_SNAKE });
export const nextFrameSnake = () => ({ type: NEXT_FRAME_SNAKE });
export const updateFrameSnake = () => ({ type: UPDATE_FRAME_SNAKE });
export const keyPressedSnake = key => ({ type: KEY_PRESSED_SNAKE, key });
export const startAnimationSnake = () => ({ type: START_ANIMATION_SNAKE });
export const stopAnimationSnake = () => ({ type: STOP_ANIMATION_SNAKE });
export const keys = {
UP: "ArrowUp",
RIGHT: "ArrowRight",
DOWN: "ArrowDown",
LEFT: "ArrowLeft",
INCREASE: "+",
DECREASE: "-"
};
export const size = 16;
const randomPosition = size => [Math.round(Math.random() * (size - 1)), Math.round(Math.random() * (size - 1))];
const comparePositions = (pos1, pos2) => pos1[0] === pos2[0] && pos1[1] === pos2[1];
const initialState = {
grid: [[]],
size,
started: false,
paused: false,
vX: 1,
vY: 0,
snake: [[0, 0], [1, 0], [2, 0]],
apple: randomPosition(size),
gameId: 0,
fps: 5
};
const 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];
};
// eslint-disable-next-line max-statements
export const reducers = (state = initialState, action) => {
if (action.type === STOP_SNAKE) {
return { ...state, started: false };
}
if (action.type === PAUSE_SNAKE) {
return { ...state, paused: !state.paused };
}
if (action.type === START_ANIMATION_SNAKE) {
if (state.isAnimating) {
return state;
}
return { ...state, isAnimating: true };
}
if (action.type === STOP_ANIMATION_SNAKE) {
if (!state.isAnimating) {
return state;
}
return { ...state, isAnimating: false };
}
if (action.type === RESET_SNAKE) {
const grid = createGrid(state.size);
const { snake, apple } = initialState;
markCell(grid, apple, "a");
snake.forEach(p => markCell(grid, p, "s"));
return { ...initialState, started: true, paused: false, grid, snake, gameId: state.gameId + 1 };
}
if (action.type === KEY_PRESSED_SNAKE) {
if (action.key === keys.UP) {
return { ...state, vX: 0, vY: -1 };
}
if (action.key === keys.DOWN) {
return { ...state, vX: 0, vY: 1 };
}
if (action.key === keys.LEFT) {
return { ...state, vX: -1, vY: 0 };
}
if (action.key === keys.RIGHT) {
return { ...state, vX: 1, vY: 0 };
}
if (action.key === keys.INCREASE) {
return { ...state, fps: state.fps + 1 };
}
if (action.key === keys.DECREASE) {
return { ...state, fps: state.fps - 1 };
}
}
if (action.type === UPDATE_FRAME_SNAKE) {
const { snake, vX, vY, size } = state;
let { apple, started, died } = state;
const grid = createGrid(size);
const newSnake = [...snake];
const nextPosition = moveSnakePart(newSnake[newSnake.length - 1], vX, vY, size);
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 (snake.filter(p => comparePositions(p, nextPosition)).length) {
started = false;
died = true;
}
markCell(grid, apple, "a");
newSnake.push(nextPosition);
newSnake.forEach(p => markCell(grid, p, "s"));
return { ...state, grid, vX, vY, snake: newSnake, apple, started, died };
}
if (action.type === "UPDATE_STATE") {
console.log("action.state", action.state);
return action.state;
}
return state;
};
export const middleware = ({ dispatch, getState }) => next => action => {
next(action);
if (action.type === START_SNAKE) {
const { started } = getState().snakeGame;
if (started) {
return;
}
dispatch(resetSnake());
animate(1, () => dispatch(nextFrameSnake()));
}
if (action.type === NEXT_FRAME_SNAKE) {
const {
snakeGame: { started, paused, gameId, fps, isAnimating }
} = getState();
if (isAnimating) {
return;
}
dispatch(startAnimationSnake());
!paused && dispatch(updateFrameSnake());
started &&
animate(fps, () => {
dispatch(stopAnimationSnake());
if (gameId !== getState().snakeGame.gameId) {
return;
}
dispatch(nextFrameSnake());
});
}
};
const animate = (fps, callback) => {
const now = Date.now();
const loop = () =>
requestAnimationFrame(() => {
if (Date.now() - now > 1000 / fps) {
return callback();
}
return loop();
});
return loop();
};

View File

@@ -1,46 +1,59 @@
const START_SNAKE = "[Snake] START_SNAKE";
const STOP_SNAKE = "[Snake] STOP_SNAKE";
const PAUSE_SNAKE = "[Snake] PAUSE_SNAKE";
const RESET_SNAKE = "[Snake] RESET_SNAKE";
const NEXT_FRAME_SNAKE = "[Snake] NEXT_FRAME_SNAKE";
const UPDATE_FRAME_SNAKE = "[Snake] UPDATE_FRAME_SNAKE";
const KEY_PRESSED_SNAKE = "[Snake] KEY_PRESSED_SNAKE";
import Module from "./module.js";
import {
registerCaller,
unregisterCaller,
startLoop,
stopLoop,
subscribeKeyPressed,
unsubscribeKeyPressed
} from "./game.js";
export const startSnake = () => ({ type: START_SNAKE });
export const stopSnake = () => ({ type: STOP_SNAKE });
export const pauseSnake = () => ({ type: PAUSE_SNAKE });
export const resetSnake = () => ({ type: RESET_SNAKE });
export const nextFrameSnake = () => ({ type: NEXT_FRAME_SNAKE });
export const updateFrameSnake = () => ({ type: UPDATE_FRAME_SNAKE });
export const keyPressedSnake = key => ({ type: KEY_PRESSED_SNAKE, key });
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",
INCREASE: "+",
DECREASE: "-"
};
export const size = 16;
const randomPosition = size => [Math.round(Math.random() * (size - 1)), Math.round(Math.random() * (size - 1))];
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];
const initialState = {
grid: [[]],
size,
started: false,
paused: false,
vX: 1,
vY: 0,
snake: [[0, 0], [1, 0], [2, 0]],
apple: randomPosition(size),
gameId: 0,
fps: 5
};
const createGrid = size => {
function createGrid(size) {
const rows = [];
for (let y = 0; y < size; y += 1) {
const columns = [];
@@ -54,7 +67,7 @@ const createGrid = size => {
rows[y] = columns;
}
return rows;
};
}
const markCell = (grid, [x, y], mark) => {
grid[y][x] = mark;
@@ -79,50 +92,61 @@ const moveSnakePart = ([x, y], vX, vY, size) => {
return [newX, newY];
};
export const reducers = (state = initialState, action) => {
if (action.type === STOP_SNAKE) {
return { ...state, started: false };
}
export const module = new Module(MODULE_NAME, initialState);
if (action.type === PAUSE_SNAKE) {
return { ...state, paused: !state.paused };
}
/* === Actions ================================================================================== */
if (action.type === RESET_SNAKE) {
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 { snake, apple } = initialState;
const { apple } = initialState;
const snake = options.started ? [[0, 0]] : [];
markCell(grid, apple, "a");
snake.forEach(p => markCell(grid, p, "s"));
return { ...initialState, started: true, paused: false, grid, snake, gameId: state.gameId + 1 };
}
return { ...state, ...initialState, grid, snake, ...options };
});
if (action.type === KEY_PRESSED_SNAKE) {
if (action.key === keys.UP) {
module.reducer(CHANGE_DIRECTION, (state, { direction }) => {
const { vX, vY } = state;
if (direction === directions.UP && vY !== 1) {
return { ...state, vX: 0, vY: -1 };
}
if (action.key === keys.DOWN) {
if (direction === directions.DOWN && vY !== -1) {
return { ...state, vX: 0, vY: 1 };
}
if (action.key === keys.LEFT) {
if (direction === directions.LEFT && vX !== 1) {
return { ...state, vX: -1, vY: 0 };
}
if (action.key === keys.RIGHT) {
if (direction === directions.RIGHT && vX !== -1) {
return { ...state, vX: 1, vY: 0 };
}
if (action.key === keys.INCREASE) {
return { ...state, fps: state.fps + 1 };
}
if (action.key === keys.DECREASE) {
return { ...state, fps: state.fps - 1 };
}
}
if (action.type === UPDATE_FRAME_SNAKE) {
return state;
});
module.reducer(UPDATE_FRAME_SNAKE, state => {
const { snake, vX, vY, size } = state;
let { apple, started, died } = 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
@@ -132,50 +156,70 @@ export const reducers = (state = initialState, action) => {
} else {
newSnake.shift();
}
if (snake.filter(p => comparePositions(p, nextPosition)).length) {
if (newSnake.filter(p => comparePositions(p, nextPosition)).length) {
started = false;
died = true;
}
markCell(grid, apple, "a");
newSnake.push(nextPosition);
newSnake.forEach(p => markCell(grid, p, "s"));
return { ...state, grid, vX, vY, snake: newSnake, apple, started, died };
}
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 };
});
return state;
};
/* === Middleware =============================================================================== */
export const middleware = ({ dispatch, getState }) => next => action => {
next(action);
module.after(UPDATE_FRAME_SNAKE, (dispatch, { died }) => {
if (died) {
dispatch(unregisterCaller(UPDATE_FRAME_SNAKE));
dispatch(stopLoop());
}
});
if (action.type === START_SNAKE) {
dispatch(resetSnake());
animate(1, () => dispatch(nextFrameSnake()));
}
module.middleware(START_SNAKE, (dispatch, { started }) => {
dispatch(resetSnake({ started: true, died: false }));
dispatch(registerCaller(UPDATE_FRAME_SNAKE));
dispatch(subscribeKeyPressed(KEY_PRESSED_SNAKE));
dispatch(startLoop());
});
if (action.type === NEXT_FRAME_SNAKE) {
const {
snakeGame: { started, paused, gameId, fps }
} = getState();
!paused && dispatch(updateFrameSnake());
started &&
animate(fps, () => {
if (gameId !== getState().snakeGame.gameId) {
return;
}
dispatch(nextFrameSnake());
});
}
};
module.middleware(STOP_SNAKE, (dispatch, { started }) => {
dispatch(unregisterCaller(UPDATE_FRAME_SNAKE));
dispatch(unsubscribeKeyPressed(KEY_PRESSED_SNAKE));
dispatch(stopLoop());
dispatch(resetSnake({ started: false }));
});
const animate = (fps, callback) => {
const now = Date.now();
const loop = () =>
requestAnimationFrame(() => {
if (Date.now() - now > 1000 / fps) {
return callback();
module.middleware(PAUSE_SNAKE, (dispatch, { paused }) => {
dispatch(updateState({ paused: !paused }));
if (paused) {
dispatch(registerCaller(UPDATE_FRAME_SNAKE));
} else {
dispatch(unregisterCaller(UPDATE_FRAME_SNAKE));
}
return loop();
});
return loop();
};
});
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());
}
});

View File

@@ -23,13 +23,14 @@ const themes = {
},
snake: {
cell: {
border: "0.0625rem solid silver",
border: "0.0625rem solid papayawhip",
size: "1.5rem"
},
cellColors: {
" ": "",
a: "green",
s: "orange"
a: "",
ab: "#90dc90",
s: "#f12f00"
}
}
}