Refactored redux code and snake game
This commit is contained in:
3
.babelrc
3
.babelrc
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
96
src/containers/Document.js
Normal file
96
src/containers/Document.js
Normal 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
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
19
src/helpers/GameLoop.js
Normal 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;
|
||||
}
|
||||
}
|
||||
46
src/index.js
46
src/index.js
@@ -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
44
src/redux/game.backup.js
Normal 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
152
src/redux/game.js
Normal 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
20
src/redux/home.js
Normal 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
244
src/redux/module.js
Normal 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
215
src/redux/snake.backup.js
Normal 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();
|
||||
};
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user