Cleaning up, refactoring and adding a sidepanel
This commit is contained in:
15
src/components/ArrowCluster.js
Normal file
15
src/components/ArrowCluster.js
Normal 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>
|
||||
// );
|
||||
22
src/components/ControlPanel.js
Normal file
22
src/components/ControlPanel.js
Normal 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;
|
||||
@@ -34,11 +34,6 @@ const HeaderMenu = () => (
|
||||
Home
|
||||
</MenuButton>
|
||||
</MenuItem>
|
||||
{
|
||||
// <MenuItem>
|
||||
// <MenuButton to="/keyboards">Keyboards</MenuButton>
|
||||
// </MenuItem>
|
||||
}
|
||||
<MenuItem>
|
||||
<MenuButton to="/snake">Snake</MenuButton>
|
||||
</MenuItem>
|
||||
|
||||
@@ -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
0
src/components/Stage.js
Normal 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);
|
||||
|
||||
@@ -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
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}`) });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
@@ -6,7 +6,7 @@ export const module = new Module(MODULE_NAME, {
|
||||
loopId: null,
|
||||
callers: [],
|
||||
keyPressSubscribers: [],
|
||||
fps: 5
|
||||
fps: 8
|
||||
});
|
||||
|
||||
export const keys = {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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
51
src/redux/store.js
Normal 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;
|
||||
Reference in New Issue
Block a user