Cleaning up, refactoring and adding a sidepanel

This commit is contained in:
2019-08-27 14:35:30 +02:00
parent eb19b675da
commit 75a6247586
20 changed files with 219 additions and 705 deletions

View File

@@ -0,0 +1,15 @@
// const ArrowButton = styled(Button)`
// min-width: 5rem;
// `;
// const ArrowCluster = () => (
// <div>
// <div>
// <ArrowButton>Up</ArrowButton>
// </div>
// <div>
// <ArrowButton>Left</ArrowButton>
// <ArrowButton>Down</ArrowButton>
// <ArrowButton>Right</ArrowButton>
// </div>
// </div>
// );

View File

@@ -0,0 +1,22 @@
import React from "react";
import styled from "styled-components";
import Button, { ToggleButton } from "../components/Button.js";
const Layout = styled.div`
display: flex;
flex-direction: column;
`;
export const ControlPanel = ({ updateFrameSnake, paused, pauseSnake, stopSnake, startSnake, fps }) => (
<Layout>
<h1>Control Panel</h1>
<ToggleButton toggle={paused} onClick={pauseSnake}>
{paused ? "Resume" : "Pause"}
</ToggleButton>
<Button onClick={stopSnake}>Stop</Button>
<Button onClick={startSnake}>Reset</Button>
<span>FPS: {fps}</span>
</Layout>
);
export default ControlPanel;

View File

@@ -34,11 +34,6 @@ const HeaderMenu = () => (
Home
</MenuButton>
</MenuItem>
{
// <MenuItem>
// <MenuButton to="/keyboards">Keyboards</MenuButton>
// </MenuItem>
}
<MenuItem>
<MenuButton to="/snake">Snake</MenuButton>
</MenuItem>

View File

@@ -47,18 +47,12 @@ const Card = styled.div`
border-bottom: dotted 2px #ad5a75;
z-index:1;
}
// &::after {
// content: "${props => props.children}";
// display: block;
// position: absolute;
// font-size: 3em;
// }
`;
const StyledScoreboard = styled.div`
height: ${props => (props.zoom || 1) * DEFAULT_HEIGHT}rem;
margin-bottom: 1rem;
white-space: nowrap;
& > div {
position: relative;

0
src/components/Stage.js Normal file
View File

View File

@@ -8,8 +8,6 @@ import Header from "./Header";
import Footer from "./Footer";
import Page from "../components/Page";
import Home from "./Home";
import Keyboards from "./Keyboards";
import KeyboardDetails from "./KeyboardDetails";
import Snake from "./Snake";
const App = ({ title, onNewTitle, getKeyboards, keyboards }) => (
@@ -18,10 +16,7 @@ const App = ({ title, onNewTitle, getKeyboards, keyboards }) => (
<Page>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/keyboards/:id" exact component={KeyboardDetails} />
<Route path="/keyboards" exact component={Keyboards} />
<Route path="/snake" exact component={Snake} />
<Route path="/test/snake" exact component={Snake} />
</Switch>
</Page>
<Footer />
@@ -37,8 +32,4 @@ const ConnectedApp = connect(
mapActions
)(App);
// const App = () => <h1>Hello Redux!!!</h1>;
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(ConnectedApp) : ConnectedApp);

View File

@@ -1,62 +1,20 @@
import React from "react";
import { Provider } from "react-redux";
import { hot } from "react-hot-loader/root";
import { ConnectedRouter } from "connected-react-router";
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 { createStore } from "../redux/store";
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 store = createStore();
const Document = ({ children }) => (
<Provider store={store}>
<ThemeProvider theme={themes.main}>
<ConnectedRouter history={history}>
<ConnectedRouter history={store.history}>
<GlobalStyle />
{children}
</ConnectedRouter>
@@ -64,33 +22,4 @@ const Document = ({ children }) => (
</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

@@ -5,19 +5,27 @@ import styled from "styled-components";
const HeaderContainer = styled.header`
background-color: ${props => props.theme.header.background};
display: flex;
min-height: 6.25rem;
align-content: space-between;
place-content: space-between;
align-items: center;
padding: 1rem;
box-sizing: border-box;
`;
const HeaderContent = styled.div`
margin: 0 auto;
width: 100%;
max-width: 60rem;
display: flex;
align-content: space-between;
place-content: space-between;
align-items: center;
`;
const Header = () => (
<HeaderContainer>
<Logo />
<HeaderMenu />
<HeaderContent>
<Logo />
<HeaderMenu />
</HeaderContent>
</HeaderContainer>
);

View File

@@ -1,53 +1,10 @@
import React from "react";
import Title from "../components/Title.js";
import Button from "../components/Button.js";
import Link from "../components/Link.js";
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";
const Home = ({
startLoop,
stopLoop,
loopId,
registerCaller,
counter,
unregisterCaller,
setFps,
fps,
callers,
resetCounter
}) => (
export const Home = () => (
<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);
export default Home;

View File

@@ -1,67 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import { getKeyboards } from "../redux/keyboards.js";
import Spinner from "../components/Spinner.js";
import Title from "../components/Title.js";
const KeyboardDetailsContainer = ({ children }) => (
<React.Fragment>
<Title large>Keyboard Details</Title>
{children}
</React.Fragment>
);
class KeyboardDetails extends React.Component {
constructor(props) {
super(props);
this.id = parseInt(this.props.match.params.id, 10);
}
componentDidMount() {
this.props.getKeyboards();
}
render() {
const keyboardsLoaded = !!this.props.keyboards;
const loading = !keyboardsLoaded || this.props.spinner;
const keyboard = this.props.keyboards && this.props.keyboards.find(k => k.id === this.id);
const keyboardExists = keyboardsLoaded && !!keyboard;
if (loading) {
return (
<KeyboardDetailsContainer>
<Spinner />
</KeyboardDetailsContainer>
);
}
if (!keyboardExists) {
return (
<KeyboardDetailsContainer>
<p>Keyboard not found...</p>
</KeyboardDetailsContainer>
);
}
return (
<KeyboardDetailsContainer>
<p>{keyboard.maker}</p>
</KeyboardDetailsContainer>
);
}
}
const state = ({ keyboards, ui }) => ({
keyboards: keyboards.list,
spinner: ui.spinner
});
const actions = {
getKeyboards
};
export default connect(
state,
actions
)(KeyboardDetails);

View File

@@ -1,54 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import Title from "../components/Title.js";
import List from "../components/List.js";
import Button from "../components/Button.js";
import Link from "../components/Link.js";
import Spinner from "../components/Spinner.js";
import { getKeyboards, showKeyboard } from "../redux/keyboards.js";
const KeyboardListItem = ({ item, showKeyboard }) => (
<List.Item key={item.id}>
<Link to={`/keyboards/${item.id}`}>
{item.maker} - {item.model}
</Link>
</List.Item>
);
const NoKeyboardsFound = ({ spinner }) => {
if (spinner) {
return <p>Loading...</p>;
}
return <p>There are no keyboards</p>;
};
class Keyboards extends React.Component {
componentDidMount() {
this.props.getKeyboards();
}
render() {
const { keyboards, spinner, getKeyboards } = this.props;
return (
<React.Fragment>
<Title large>Keyboard List</Title>
<List items={keyboards} Item={KeyboardListItem} Empty={NoKeyboardsFound} itemProps={this.props} />
<Button onClick={() => getKeyboards({ force: true })}>Refresh</Button>
{spinner && <Spinner />}
</React.Fragment>
);
}
}
const mapState = ({ keyboards, ui }) => ({
keyboards: keyboards.list,
spinner: ui.spinner
});
const mapActions = {
getKeyboards,
showKeyboard
};
export default connect(
mapState,
mapActions
)(Keyboards);

View File

@@ -15,11 +15,23 @@ import {
} from "../redux/snake.js";
/* Components */
import Title from "../components/Title.js";
import Button, { ToggleButton } from "../components/Button.js";
import ControlPanel from "../components/ControlPanel.js";
import Scoreboard from "../components/Scoreboard.js";
const Row = styled.div``;
const Layout = styled.div`
display: flex;
width: 100%;
place-content: space-between;
`;
const SidePanel = styled.div`
display: flex;
flex-direction: column;
`;
const Row = styled.div`
white-space: nowrap;
`;
const Cell = styled.div`
width: ${({ theme }) => theme.snake.cell.size};
@@ -35,7 +47,7 @@ const Cell = styled.div`
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%)`};
background-color: ${props => props.value && `hsl(340, ${props.value.brightness}%, 65%)`};
box-shadow: 1px 1px 2px #ddd inset, -1px -1px 2px gray inset;
`;
@@ -48,7 +60,7 @@ const Apple = styled.div.attrs({ role: "img", "aria-label": "apple" })`
line-height: 1.8rem;
padding-left: 0.0625rem;
&:after {
content: "${props => (!props.died ? "🍎" : "")}";
content: "${props => (!props.died ? "🍎" : "🐛")}";
display: inline-block;
}
`;
@@ -60,44 +72,30 @@ const Skull = styled.div.attrs({ role: "img", "aria-label": "skull" })`
font-size: 1.3rem;
vertical-align: top;
line-height: 1.8rem;
background-color: ${props => props.value && `hsl(0, 0%, ${100 - props.value.slice(1)}% )`};
background-color: ${props => props.value && `hsl(0, 0%, ${100 - props.value.brightness}% )`};
padding-left: 0.0625rem;
box-shadow: 1px 1px 2px #ddd inset, -1px -1px 2px gray inset;
&:after {
content: "💀";
content: "${props => props.value && props.value.index === 0 && "💀"}";
display: inline-block;
}
`;
const Grid = ({ data, died }) =>
data.map((r, y) => (
<Row key={`y${y}`}>
{r.map((c, x) => (
<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 ArrowButton = styled(Button)`
// min-width: 5rem;
// `;
// const ArrowCluster = () => (
// <div>
// <div>
// <ArrowButton>Up</ArrowButton>
// </div>
// <div>
// <ArrowButton>Left</ArrowButton>
// <ArrowButton>Down</ArrowButton>
// <ArrowButton>Right</ArrowButton>
// </div>
// </div>
// );
const Grid = ({ data, died }) => (
<div>
{data.map((r, y) => (
<Row key={`y${y}`}>
{r.map((c, x) => (
<Cell key={`x${x}y${y}`} died={died}>
{c.type === "apple" && <Apple died={died} />}
{!died && c.type === "snake" && <SnakePart value={c} />}
{died && c.type === "snake" && <Skull value={c} />}
</Cell>
))}
</Row>
))}
</div>
);
class Snake extends React.Component {
constructor(props) {
@@ -133,15 +131,20 @@ class Snake extends React.Component {
return (
<React.Fragment>
<Scoreboard score={score} zoom={2} />
<Grid data={grid} died={died} />
<Button onClick={updateFrameSnake}>Next</Button>
<ToggleButton toggle={paused} onClick={pauseSnake}>
{paused ? "Resume" : "Pause"}
</ToggleButton>
<Button onClick={stopSnake}>Stop</Button>
<Button onClick={startSnake}>Reset</Button>
<span>FPS: {fps}</span>
<Layout>
<Grid data={grid} died={died} />
<SidePanel>
<Scoreboard score={score} zoom={2} />
<ControlPanel
updateFrameSnake={updateFrameSnake}
paused={paused}
pauseSnake={pauseSnake}
stopSnake={stopSnake}
startSnake={startSnake}
fps={fps}
/>
</SidePanel>
</Layout>
</React.Fragment>
);
}

View File

@@ -1,22 +0,0 @@
const FETCH_DATA = "[API] FETCH_DATA";
export const fetchData = ({ url, onError, onSuccess }) => ({ type: FETCH_DATA, url, onError, onSuccess });
const keyboardList = [
{ id: 1, maker: "Percent Studio", model: "Canoe" },
{ id: 2, maker: "Gray Studio", model: "Space65" }
];
export const middleware = ({ dispatch }) => next => action => {
next(action);
if (action.type === FETCH_DATA) {
if (action.url === "/keyboards") {
setTimeout(() => {
dispatch({ type: action.onSuccess, data: keyboardList });
}, 3000);
} else {
dispatch({ type: action.onError, data: new Error(`Unable to fetch data from url ${action.url}`) });
}
}
};

View File

@@ -1,44 +0,0 @@
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;

View File

@@ -6,7 +6,7 @@ export const module = new Module(MODULE_NAME, {
loopId: null,
callers: [],
keyPressSubscribers: [],
fps: 5
fps: 8
});
export const keys = {

View File

@@ -1,20 +0,0 @@
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;

View File

@@ -1,65 +0,0 @@
import { push } from "connected-react-router";
import { showSpinner, hideSpinner } from "./ui.js";
import { fetchData } from "./api.js";
const GET_KEYBOARDS = "[Keyboards] GET_KEYBOARDS";
const LOAD_KEYBOARDS = "[Keyboards] LOAD_KEYBOARDS";
const LOAD_KEYBOARDS_SUCCEEDED = "[Keyboards] LOAD_KEYBOARDS_SUCCEEDED";
const LOAD_KEYBOARDS_FAILED = "[Keyboards] LOAD_KEYBOARDS_FAILED";
const UPDATE_KEYBOARDS = "[Keyboards] UPDATE_KEYBOARDS";
const GET_KEYBOARD = "[Keyboards] GET_KEYBOARD";
const SHOW_KEYBOARD_DETAILS = "[Keyboards] SHOW_KEYBOARD_DETAILS";
export const loadKeyboards = ({ onError, onSuccess }) => ({ type: LOAD_KEYBOARDS, onError, onSuccess });
export const getKeyboards = ({ force } = { force: false }) => ({ type: GET_KEYBOARDS, force });
export const updateKeyboards = keyboards => ({ type: UPDATE_KEYBOARDS, keyboards });
export const showKeyboard = id => ({ type: SHOW_KEYBOARD_DETAILS, id });
export const getKeyboard = id => ({ type: GET_KEYBOARD, id });
export const middleware = ({ dispatch, getState }) => next => action => {
next(action);
if (action.type === SHOW_KEYBOARD_DETAILS) {
dispatch(push(`/keyboards/${action.id}`));
}
if (action.type === GET_KEYBOARDS) {
const { keyboards } = getState();
if (!keyboards.list || action.force) {
dispatch(showSpinner());
dispatch(loadKeyboards({ onError: LOAD_KEYBOARDS_FAILED, onSuccess: LOAD_KEYBOARDS_SUCCEEDED }));
}
}
if (action.type === LOAD_KEYBOARDS) {
dispatch(fetchData({ url: "/keyboards", onError: action.onError, onSuccess: action.onSuccess }));
}
if (action.type === LOAD_KEYBOARDS_SUCCEEDED) {
dispatch(hideSpinner());
dispatch(updateKeyboards(action.data));
}
};
export const reducers = (keyboards = {}, action) => {
if (action.type === UPDATE_KEYBOARDS) {
return {
...keyboards,
list: action.keyboards,
error: undefined
};
}
if (action.type === LOAD_KEYBOARDS_FAILED) {
return {
...keyboards,
list: undefined,
error: action.err
};
}
if (action.type === GET_KEYBOARD) {
return {
...keyboards
};
}
return keyboards;
};

View File

@@ -1,215 +0,0 @@
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

@@ -17,6 +17,8 @@ const initialState = {
paused: false,
vX: 1,
vY: 0,
vXNext: undefined,
vYNext: undefined,
snake: [],
score: 0,
apple: []
@@ -42,7 +44,7 @@ export const keys = {
k: "k",
l: "l",
p: "p",
snakePart: "snakePart",
s: "s",
r: "r"
};
@@ -52,6 +54,8 @@ function randomPosition(size) {
}
const comparePositions = (pos1, pos2) => pos1[0] === pos2[0] && pos1[1] === pos2[1];
const brightness = (l, i) => 20 + Math.round(0.8 * ((i + 1) / l) * 100);
function createGrid(size) {
const rows = [];
for (let y = 0; y < size; y += 1) {
@@ -118,55 +122,87 @@ module.reducer(RESET_SNAKE, (state, options) => {
const apple = options.started ? randomPosition(state.size) : initialState.apple;
const snake = options.started ? [[0, 0]] : initialState.snake;
markCell(grid, apple, "a");
snake.forEach(p => markCell(grid, p, "s"));
markCell(grid, apple, { type: "apple" });
snake.forEach((snakePart, i) =>
markCell(grid, snakePart, { type: "snake", index: snake.length - 1 - i, brightness: brightness(snake.length, i) })
);
return { ...state, ...initialState, grid, snake, apple, ...options };
});
module.reducer(CHANGE_DIRECTION, (state, { direction }) => {
const { vX, vY } = state;
const { vX, vY, vXNext, vYNext } = state;
if (vXNext !== undefined || vYNext !== undefined) {
return state;
}
if (direction === directions.UP && vY !== 1) {
return { ...state, vX: 0, vY: -1 };
return { ...state, vXNext: 0, vYNext: -1 };
}
if (direction === directions.DOWN && vY !== -1) {
return { ...state, vX: 0, vY: 1 };
return { ...state, vXNext: 0, vYNext: 1 };
}
if (direction === directions.LEFT && vX !== 1) {
return { ...state, vX: -1, vY: 0 };
return { ...state, vXNext: -1, vYNext: 0 };
}
if (direction === directions.RIGHT && vX !== -1) {
return { ...state, vX: 1, vY: 0 };
return { ...state, vXNext: 1, vYNext: 0 };
}
return state;
});
module.reducer(UPDATE_FRAME_SNAKE, state => {
const { snake, vX, vY, size } = state;
const { snake, vX, vY, vXNext, vYNext, size } = state;
let { apple, started, died, score } = 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 {
const nextPosition = moveSnakePart(
snake[snake.length - 1],
vXNext !== undefined ? vXNext : vX,
vYNext !== undefined ? vYNext : vY,
size
);
const newSnake = [...snake, nextPosition];
/* If the snake did not eat an apple then remove the last part of its tail */
if (!comparePositions(nextPosition, apple)) {
newSnake.shift();
}
if (newSnake.filter(p => comparePositions(p, nextPosition)).length) {
/* Check if the snake hits itself */
if (newSnake.slice(0, -1).filter((snakePart, index) => comparePositions(snakePart, 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)));
if (comparePositions(nextPosition, apple)) {
apple = randomPosition(size);
// eslint-disable-next-line no-loop-func
while (newSnake.filter(snakePart => comparePositions(snakePart, apple)).length) {
apple = randomPosition(size);
}
}
markCell(grid, apple, { type: "apple" });
newSnake.forEach((snakePart, i) =>
markCell(grid, snakePart, {
type: "snake",
index: newSnake.length - 1 - i,
brightness: brightness(newSnake.length, i)
})
);
score = newSnake.length;
return { ...state, grid, vX, vY, snake: newSnake, apple, started, died, score };
return {
...state,
grid,
vX: vXNext !== undefined ? vXNext : vX,
vY: vYNext !== undefined ? vYNext : vY,
vXNext: undefined,
vYNext: undefined,
snake: newSnake,
apple,
started,
died,
score
};
});
/* === Middleware =============================================================================== */
@@ -217,7 +253,7 @@ module.middleware(KEY_PRESSED_SNAKE, (dispatch, _, { key }) => {
if (key === keys.s) {
dispatch(stopSnake());
}
if (key === keys.p) {
if (key === keys.snakePart) {
dispatch(pauseSnake());
}
if (key === keys.r) {

51
src/redux/store.js Normal file
View File

@@ -0,0 +1,51 @@
import { createStore as createReduxStore, combineReducers, compose, applyMiddleware } from "redux";
import { createBrowserHistory } from "history";
import { connectRouter, routerMiddleware } from "connected-react-router";
import { reducers as uiReducers } from "./ui.js";
import { module as snakeModule } from "./snake.js";
import { module as gameModule } from "./game.js";
export const createStore = () => {
const history = createBrowserHistory({
basename: process.env.NODE_ENV === "production" ? "/redux/" : "/"
});
const store = createReduxStore(
combineReducers({
router: connectRouter(history),
ui: uiReducers,
...snakeModule.reducers,
...gameModule.reducers
}),
{},
compose.apply(
this,
[
applyMiddleware(routerMiddleware(history), snakeModule.middlewares, gameModule.middlewares),
process.env.NODE_ENV !== "production" &&
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
].filter(m => m)
)
);
if (module.hot) {
module.hot.accept(["./ui.js", "./snake.js", "./game.js", "./module.js"], () => {
store.replaceReducer(
combineReducers({
ui: uiReducers,
router: connectRouter(history),
...snakeModule.reducers,
...gameModule.reducers
})
);
});
}
store.history = history;
return store;
};
export default createStore;