Update about page and documentation. Add allow wrapping option.

This commit is contained in:
2019-09-10 21:39:27 +02:00
parent 9fc48df1ce
commit 3a8ef7e8f2
36 changed files with 535 additions and 111 deletions

View File

@@ -2,28 +2,34 @@
Play snake in your browser with HTML 5. This game is developed using React with hooks and Redux. All the game state and highscores are stored in local storage.
## Introduction
This game has been created as a Crafity experiment to test out React, Redux and Styled Components and see if it is possible to create a simple web based game like snake. The for this game is open source and free to download and change.
A couple of noticeable features in this game are resumable game play using local storage. The page can be reloaded or reopened any time and the game should continue from where it was left.
## Feature Technologies
- React + React Router
- React
- React Router
- Redux
- Styled Components
- Webpack + Babel
- Webpack
- Babel
- Font Awesome
- Jest
- ESLint
- pre-commit
- Docker
## Todo
## TODO
[ ] Create welcome / instructions page / about page
[ ] Link to repository
[x] Add license
[x] High score
[ ] Docker build script
[ ] Improve banner / popup windows
[ ] Make stage wrapping an optional feature
[ ] Touch and mobile support
## License
```
The MIT License (MIT)
Copyright (c) Crafity VOF <info@crafity.com> (crafity.com)
@@ -45,3 +51,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
```

View File

@@ -5,8 +5,15 @@ server {
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
root /usr/share/nginx/html;
location /redux/img {
alias /usr/share/nginx/html/img;
}
location /img {}
location / {
alias /usr/share/nginx/html/;
try_files $uri /index.html;
}

View File

@@ -4,7 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server --mode development --open",
"dev": "webpack-dev-server --mode development",
"build": "webpack --optimize-minimize --config webpack.production.config.js",
"webpack": "webpack --config webpack.config.js --watch",
"lint": "eslint src",
@@ -52,6 +52,7 @@
"react-redux": "^7.1.0",
"react-router": "^5.0.1",
"react-router-dom": "^5.0.1",
"react-router-hash-link": "^1.2.2",
"react-transition-group": "^4.2.2",
"redux": "^4.0.4",
"styled-components": "^4.3.2"

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
public/img/docker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/img/font-awesome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
public/img/jest.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
public/img/reactjs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
public/img/reduxjs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
public/img/snake.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
public/img/webpack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Redux Demo</title>
<title>Crafity Snake - A React/Redux Demo</title>
<link href="https://fonts.googleapis.com/css?family=Lato:100,400|Raleway:100,400,700|Rubik:500" rel="stylesheet" />
<link
rel="stylesheet"
@@ -16,6 +16,13 @@
integrity="sha384-NnhYAEceBbm5rQuNvCv6o4iIoPZlkaWfvuXVh4XkRNvHWKgu/Mk2nEjFZpPQdwiz"
crossorigin="anonymous"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.4/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-router/5.0.1/react-router.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/styled-components/4.3.2/styled-components.min.js"></script>
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/react-router-dom/5.0.1/react-router-dom.min.js"></script> -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.9.0-alpha.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.1.1/react-redux.min.js"></script>
</head>
<body>
<div id="root"></div>

View File

@@ -0,0 +1,49 @@
import styled from "styled-components";
export const Checkbox = styled.input.attrs({ type: "checkbox" })`
appearance: none;
border-width: ${({ theme }) => theme.checkbox.borderWidth};
border-style: solid;
border-radius: ${({ theme }) => theme.checkbox.borderRadius};
border-color: ${({ theme }) => theme.checkbox.borderColor};
height: 16px;
width: 16px;
position: relative;
&:checked {
&:before {
position: absolute;
content: "";
display: block;
top: 0;
transform: rotate(-45deg);
border-bottom: 1px solid black;
border-left: 1px solid black;
border-width: 3px;
border-color: ${({ theme }) => theme.checkbox.checked.background};
width: 10px;
height: 5px;
}
}
&:focus,
&:hover {
outline: ${({ disabled }) => (!disabled ? "1px solid silver" : "none")};
}
&:hover {
background-color: ${({ theme }) => theme.button.hover.background};
}
&:active,
&.active {
border-color: ${({ theme }) => theme.button.active.borderColor};
color: ${({ theme }) => theme.button.active.color};
background-color: ${({ theme }) => theme.button.active.background};
}
`;
export default Checkbox;

View File

@@ -2,10 +2,12 @@ import styled from "styled-components";
export const Code = styled.code`
display: inline-block;
color: white;
background-color: gray;
padding: 0 0.2rem;
line-height: 1.4rem;
font-weight: bold;
background-color: ${({ theme }) => theme.button.background};
color: ${({ theme }) => theme.button.color};
padding: 0 0.3rem;
line-height: 1.5rem;
border-radius: 0.125rem;
`;
export default Code;

View File

@@ -1,6 +1,8 @@
import React from "react";
import styled from "styled-components";
import { IconButton, ToggleIconButton } from "../components/Button.js";
import Link from "../components/Link.js";
import Checkbox from "../components/Checkbox.js";
const buttonSize = 2.5;
@@ -33,9 +35,26 @@ export const ControlPanel = ({
zoom,
setFps,
zoomIn,
zoomOut
zoomOut,
allowWrapping,
changeAllowWrapping
}) => (
<Layout>
<HorizontalStack style={{ minHeight: "2rem", lineHeight: "2rem" }}>
<Link smooth to="/about#how-to-play">
How to play?
</Link>
</HorizontalStack>
<HorizontalStack style={{ minHeight: "2rem", lineHeight: "2rem" }}>
<label htmlFor="allow-wrapping">Allow wrapping:</label>
<div style={{ display: "inline-block" }}>
<Checkbox
id="allow-wrapping"
checked={allowWrapping}
onChange={({ target: { checked } }) => changeAllowWrapping(checked)}
/>
</div>
</HorizontalStack>
<HorizontalStack style={{ minHeight: "2rem", lineHeight: "2rem" }}>
<span>
Zoom: <b>{zoom}</b>

View File

@@ -48,7 +48,6 @@ const HighscoreEntry = styled.li`
`;
const Highscore = ({ highscores = [] }) => {
console.log("highscores", highscores);
return (
<HighscoreContainer>
<Title>Highscore</Title>

View File

@@ -1,5 +1,6 @@
import styled from "styled-components";
import { NavLink } from "react-router-dom";
import { HashLink } from "react-router-hash-link";
const SharedStyle = () => `
color: gray;
@@ -19,6 +20,13 @@ const StyledNavLink = styled(NavLink)`
}
`;
const StyledHashLink = styled(HashLink)`
${SharedStyle}
&.active {
color: black;
}
`;
const Link = (props, refs) => {
const updatedProps = { ...props };
@@ -26,7 +34,11 @@ const Link = (props, refs) => {
updatedProps.onClick = e => e.preventDefault() || props.onClick();
}
return props.to ? StyledNavLink.render(updatedProps) : StyledLink.render(updatedProps);
return props.to
? props.to.match("#")
? StyledHashLink.render(updatedProps)
: StyledNavLink.render(updatedProps)
: StyledLink.render(updatedProps);
};
export default Link;

View File

@@ -6,8 +6,11 @@ export const Content = styled.div`
content === PageSection.content.center &&
css`
max-width: 60rem;
min-height: 31.25rem;
display: flex;
flex-direction: column;
justify-content: center;
margin: 0 auto;
// padding: 0.5em;
`}
`;
@@ -16,10 +19,12 @@ const contentPosition = {
stretch: "stretch"
};
export const PageSection = ({ content, children, className } = { content: contentPosition.stretch }) => {
export const PageSection = ({ content, children, className, center, stretch } = {}) => {
return (
<div className={className}>
<Content content={content}>{children}</Content>
<Content content={content || (center && contentPosition.center) || (stretch && contentPosition.stretch)}>
<div>{children}</div>
</Content>
</div>
);
};

View File

@@ -22,12 +22,12 @@ const Cell = styled.div.attrs(({ theme, zoom }) => ({
box-shadow: ${({ theme }) => theme.stage.cell.boxShadow};
`;
export const Stage = ({ data, children, zoom = 1 }) => (
export const Stage = ({ grid, children, zoom = 1 }) => (
<div>
{data.map((r, y) => (
<Row key={`y${y}`} zoom={zoom}>
{grid.map((r, y) => (
<Row key={`y${y}`} {...{ zoom }}>
{r.map((c, x) => (
<Cell key={`x${x}y${y}`} zoom={zoom}>
<Cell key={`x${x}y${y}`} {...{ zoom }}>
{children(c, zoom)}
</Cell>
))}

View File

@@ -38,6 +38,7 @@ const ThemeSelector = ({ theme, changeTheme, border }) => {
<StyledSelect border={border} onChange={e => changeTheme(e.target.value)} value={theme}>
<option value="light">Light</option>
<option value="dark">Miami Night</option>
<option value="forestNight">Forest Night</option>
<option value="darkOcean">Dark Ocean</option>
<option value="default">Black / White</option>
</StyledSelect>

View File

@@ -1,10 +1,19 @@
import styled, { css } from "styled-components";
const Title = styled.h1`
const Title = styled.h1.attrs(({ id }) => ({
id
}))`
margin: 0;
font-size: 1.5rem;
line-height: 4rem;
${props =>
props.smaller &&
css`
font-size: 1.2rem;
line-height: 3rem;
`}
${props =>
props.large &&
css`

View File

@@ -3,6 +3,8 @@ import styled, { css } from "styled-components";
import Title from "../components/Title.js";
import PageSection from "../components/PageSection.js";
import Code from "../components/Code.js";
import Button from "../components/Button.js";
import Link from "../components/Link.js";
const MainPageSection = styled(PageSection)`
background-color: ${({ theme }) => theme.colors.pageSectionMainColor};
@@ -54,9 +56,32 @@ const Layout = styled.div`
`}
`;
const StyledAboutPage = styled.div`
font-size: 1.5rem;
`;
const KeyMapTable = styled.table`
margin: 3rem 0;
thead td {
font-size: 0.7em;
color: ${({ theme }) => theme.colors.colorAlternate};
}
td:nth-child(1),
td:nth-child(2) {
padding: 0 1rem;
text-align: center;
}
td:nth-child(3) {
padding: 0 1rem;
line-height: 2.3rem;
font-size: 0.8em;
}
`;
export const About = () => (
<React.Fragment>
<MainPageSection content={PageSection.content.center}>
<StyledAboutPage>
<MainPageSection center>
<Layout style={{ justifyContent: "center" }} direction={"horizontal"}>
<Layout style={{ width: "30rem", placeContent: "center" }} direction={"vertical"}>
<Title larger>Crafity Snake</Title>
@@ -65,58 +90,204 @@ export const About = () => (
<Icon>🐍</Icon>
</Layout>
</MainPageSection>
<StandardPageSection content={PageSection.content.center}>
<StandardPageSection center>
<Title>Introduction</Title>
<p>
This experiment has been created to test out React, Redux and Styled Components and see if it is possible to
create a simple web based game like snake. The source code for this game is open source and free to download and
change.
This game has been created as a <b>Crafity</b> experiment to test out React, Redux and Styled Components and see
if it is possible to create a simple web based game like snake. The{" "}
<Link smooth to="/about#source-code">
source code
</Link>{" "}
for this game is open source and free to download and change.
</p>
<p>
A couple of noticable features in this game are resumable game play using local storage. The page can be
A couple of noticeable features in this game are resumable game play using local storage. The page can be
reloaded or reopened any time and the game should continue from where it was left.
</p>
</StandardPageSection>
<AlternatePageSection content={PageSection.content.center}>
<Title>How to play</Title>
<AlternatePageSection center>
<Title id="how-to-play">How to play</Title>
<p>
You control the snake using the arrow keys or the <Code>hjkl</Code> keys. Everytime the snake eats an apple the
tail of the snake grows longer. When the snake touches his own body the game is over.
In this game you play a snake and the goal is to eat as many apples as possible. Every time your snake eats an
apple its tail grows a little bit longer and you earn a point. If your snake touches its own tail or one of the
four walls* it will die.
</p>
<p>
Other keys to control the game are <Code>r</Code> to (re)start the game. The <Code>s</Code> key to stop the game
and <Code>p</Code> to pause the game.
<i>* Only when allow wrapping is turned off</i>
</p>
<KeyMapTable>
<thead>
<tr>
<td>Key</td>
<td>Alternative</td>
<td />
</tr>
</thead>
<tbody>
<tr>
<td>
<Code>r</Code>
</td>
<td />
<td>(Re)start the game</td>
</tr>
<tr>
<td>
<Code>s</Code>
</td>
<td />
<td>Stop the game</td>
</tr>
<tr>
<td>
<Code>p</Code>
</td>
<td />
<td>Pause the game</td>
</tr>
<tr>
<td>
<Code>h</Code>
</td>
<td>
<Code></Code>
</td>
<td>Left</td>
</tr>
<tr>
<td>
<Code>j</Code>
</td>
<td>
<Code></Code>
</td>
<td>Down</td>
</tr>
<tr>
<td>
<Code>k</Code>
</td>
<td>
<Code></Code>
</td>
<td>Up</td>
</tr>
<tr>
<td>
<Code>l</Code>
</td>
<td>
<Code></Code>
</td>
<td>Right</td>
</tr>
<tr>
<td>
<Code>w</Code>
</td>
<td>
<Code />
</td>
<td>Allow Wrapping</td>
</tr>
<tr>
<td>
<Code>+</Code>
</td>
<td>
<Code>=</Code>
</td>
<td>Increase FPS</td>
</tr>
<tr>
<td>
<Code>-</Code>
</td>
<td />
<td>Decrease FPS</td>
</tr>
<tr>
<td>
<Code>0...9</Code>
</td>
<td />
<td>Zoom level</td>
</tr>
</tbody>
</KeyMapTable>
<p style={{ textAlign: "center", margin: "2rem 0" }}>
<Button
style={{ width: "10rem", height: "3rem", lineHeight: "3rem", fontWeight: "bold", fontSize: "1.5rem" }}
exact
to="/">
Play now
</Button>
</p>
</AlternatePageSection>
<StandardPageSection content={PageSection.content.center}>
<StandardPageSection center>
<Title>Technology</Title>
<p>Snake has been developed with the folowing technologies.</p>
<ul>
<li>React + React Router</li>
<li>Redux</li>
<li>Styled Components</li>
<li>Webpack + Babel</li>
<li>Jest</li>
<li>ESLint</li>
<li>pre-commit</li>
<li>Docker</li>
</ul>
<Layout style={{ justifyContent: "space-around" }}>
<ul>
<li>React</li>
<li>React Router</li>
<li>Redux</li>
<li>Styled Components</li>
<li>Webpack</li>
<li>Babel</li>
</ul>
<ul>
<li>Font Awesome</li>
<li>Jest</li>
<li>ESLint</li>
<li>pre-commit</li>
<li>Docker</li>
</ul>
</Layout>
<Layout style={{ justifyContent: "space-around", height: "64px", marginBottom: "1rem" }}>
<img src="img/reactjs.png" title="React" alt="React Logo" />
<img src="img/reduxjs.png" title="Redux" alt="Redux Logo" />
<img src="img/styled-components.png" title="Styled Components" alt="Styled Components Logo" />
<img src="img/font-awesome.png" title="Font Awesome" alt="Font Awesome Logo" />
<img src="img/webpack.png" title="Webpack" alt="Webpack Logo" />
<img src="img/jest.png" title="Jest" alt="Jest Logo" />
<img src="img/docker.png" title="Docker" alt="Docker Logo" />
</Layout>
<Title smaller>State Management</Title>
<p>
All the game and UI state is managed with Redux. This means all the state is stored in a central state store.
Every second the state is serialized and stored in the browser&quot;s localStorage. This makes the game fully
resumable.
</p>
<p>Change is changed using Redux reducers and the orchistration of actions is handled by Redux Middleware.</p>
<Title smaller>Styling</Title>
<p>
All the styling and theming is handled by the module styled-components. Styled-components let&quot;s you style
your react components directly from your Javascript code.
</p>
</StandardPageSection>
<AlternatePageSection content={PageSection.content.center}>
<Title>Source Code</Title>
<p>The source code is hosted on Crafity&apos;s git repositories at the following location:</p>
<AlternatePageSection center>
<Title id="source-code">Source Code</Title>
<p>The source code is hosted on Crafity&apos;s Gitea environment at the following location:</p>
<p>
<a href="https://git.crafity.com">Crafity Snake</a>
</p>
<p>After donwloading the source code run the following commands:</p>
<p>
<Code>npm install</Code>
<Link target="_blank" href="https://git.crafity.com/Crafity/snake">
Download here
</Link>
</p>
<p>
<Code>npm run dev</Code>
The source code is MIT licensed. Feel free to make modifications and share. Suggestions and pull requests are
welcome. Use Crafity&quot;s git repository to file issues and pull requests.
</p>
</AlternatePageSection>
</React.Fragment>
<StandardPageSection center>
<Title>Running Snake</Title>
<p>After donwloading the source code run the following commands:</p>
<Code style={{ display: "block", padding: "1rem" }}>
<p>$ npm install</p>
<p>$ npm run dev</p>
</Code>
</StandardPageSection>
</StyledAboutPage>
);
export default About;

7
src/containers/Empty.js Normal file
View File

@@ -0,0 +1,7 @@
import React from "react";
export const Empty = () => {
return <h1>lskdfj</h1>;
};
export default Empty;

View File

@@ -12,6 +12,7 @@ import {
pauseSnake,
updateFrameSnake,
keyPressedSnake,
changeAllowWrapping,
zoomIn,
zoomOut
} from "../redux/snake.js";
@@ -68,7 +69,9 @@ const Snake = ({
lastGameId,
zoom,
zoomIn,
zoomOut
zoomOut,
allowWrapping,
changeAllowWrapping
}) => {
const onSubmitHighscore = result => {
if (result) {
@@ -82,10 +85,10 @@ const Snake = ({
<React.Fragment>
<Layout>
<StageContainer>
<Stage data={grid} zoom={zoom}>
<Stage {...{ zoom, grid }}>
{cell =>
(cell.type === "apple" && <Apple zoom={zoom} died={died} />) ||
(cell.type === "snake" && <SnakePart zoom={zoom} value={cell} died={died} />)
(cell.type === "apple" && <Apple {...{ zoom, died }} />) ||
(cell.type === "snake" && <SnakePart value={cell} {...{ zoom, died }} />)
}
</Stage>
{!started && (
@@ -101,26 +104,30 @@ const Snake = ({
</Banner>
)}
{died && lastGameId !== gameId && hasHighscore(score) && (
<HighscoreInput score={score} gameId={gameId} name={lastUsername} onSubmit={onSubmitHighscore} />
<HighscoreInput name={lastUsername} onSubmit={onSubmitHighscore} {...{ score, gameId }} />
)}
</StageContainer>
<SidePanel>
<Scoreboard score={score} zoom={3} />
<Scoreboard {...{ score }} zoom={3} />
<Highscore />
<ControlPanel
updateFrameSnake={updateFrameSnake}
started={started}
paused={paused}
pauseSnake={pauseSnake}
stopSnake={stopSnake}
startSnake={startSnake}
theme={theme}
changeTheme={changeTheme}
fps={fps}
setFps={setFps}
zoom={zoom}
zoomIn={zoomIn}
zoomOut={zoomOut}
{...{
updateFrameSnake,
started,
paused,
pauseSnake,
stopSnake,
startSnake,
theme,
changeTheme,
fps,
setFps,
zoom,
zoomIn,
zoomOut,
allowWrapping,
changeAllowWrapping
}}
/>
</SidePanel>
</Layout>
@@ -148,7 +155,8 @@ const mapActionsToProps = {
zoomOut,
changeTheme,
registerHighscore,
skipHighscore
skipHighscore,
changeAllowWrapping
};
export default connect(

View File

@@ -16,6 +16,7 @@ export const keys = {
p: "p",
s: "s",
r: "r",
w: "w",
1: "1",
2: "2",

View File

@@ -1,6 +1,6 @@
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { Provider, ReactReduxContext } from "react-redux";
import { ConnectedRouter } from "connected-react-router";
import { createStore } from "./redux/store";
@@ -13,8 +13,8 @@ const store = createStore();
window.store = store;
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={store.history}>
<Provider store={store} context={ReactReduxContext}>
<ConnectedRouter history={store.history} context={ReactReduxContext}>
<App />
</ConnectedRouter>
</Provider>,

View File

@@ -214,7 +214,6 @@ export class Module {
select(...items) {
return (items || []).reduce((selection, item) => {
console.log("item", item);
return selection;
}, {});
}

View File

@@ -10,6 +10,7 @@ const DEFAUL_ZOOM_STEP = 0.1;
const initialState = {
grid: createGrid(DEFAULT_GRID_SIZE),
zoom: 1.5,
allowWrapping: false,
size: DEFAULT_GRID_SIZE,
paused: false,
gameId: 0,
@@ -64,9 +65,12 @@ const updateGrid = ({ grid, apple, snake }) => {
);
};
const moveSnakePart = ([x, y], vX, vY, size) => {
const moveSnakePart = ([x, y], vX, vY, size, allowWrapping) => {
let newX = x + vX;
let newY = y + vY;
if (!allowWrapping) {
return [newX, newY];
}
if (newX > size - 1) {
newX = 0;
}
@@ -82,6 +86,22 @@ const moveSnakePart = ([x, y], vX, vY, size) => {
return [newX, newY];
};
const isOutOfBound = ([x, y], size) => {
if (x > size - 1) {
return true;
}
if (x < 0) {
return true;
}
if (y > size - 1) {
return true;
}
if (y < 0) {
return true;
}
return false;
};
export const module = new Module(MODULE_NAME, initialState);
/* === Actions ================================================================================== */
@@ -97,6 +117,11 @@ export const [ZOOM_OUT, zoomOut] = module.action("ZOOM_OUT", step => ({ step }))
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 [CHANGE_ALLOW_WRAPPING, changeAllowWrapping] = module.action("CHANGE_ALLOW_WRAPPING", allowWrapping => ({
allowWrapping
}));
export const [TOGGLE_ALLOW_WRAPPING, toggleAllowWrapping] = module.action("TOGGLE_ALLOW_WRAPPING");
export const [UPDATE_STATE, updateState] = module.action("UPDATE_STATE", (newState, fullUpdate = false) => ({
newState,
fullUpdate
@@ -117,11 +142,12 @@ module.reducer(ZOOM_OUT, (state, { step = DEFAUL_ZOOM_STEP }) =>
module.reducer(RESET_SNAKE, (state, options) => {
const grid = createGrid(state.size);
const zoom = state.zoom;
const allowWrapping = state.allowWrapping;
const gameId = (state.gameId || 0) + 1;
const apple = options.started ? randomPosition(state.size) : initialState.apple;
const snake = options.started ? [[0, 0]] : initialState.snake;
updateGrid({ grid, apple, snake });
return { ...state, ...initialState, grid, zoom, snake, apple, gameId, ...options };
return { ...state, ...initialState, grid, allowWrapping, zoom, snake, apple, gameId, ...options };
});
module.reducer(CHANGE_DIRECTION, (state, { direction }) => {
@@ -145,31 +171,39 @@ module.reducer(CHANGE_DIRECTION, (state, { direction }) => {
});
module.reducer(UPDATE_FRAME_SNAKE, state => {
const { snake, vX, vY, next, size } = state;
const { snake, vX, vY, next, size, allowWrapping } = state;
let { apple, started, died, score } = state;
const grid = createGrid(size);
const [vXNext, vYNext] = next.length ? next[0] : [vX, vY];
const nextPosition = moveSnakePart(snake[snake.length - 1], vXNext, vYNext, size);
const newSnake = [...snake, nextPosition];
const nextPosition = moveSnakePart(snake[snake.length - 1], vXNext, vYNext, size, allowWrapping);
let newSnake;
/* If the snake did not eat an apple then remove the last part of its tail */
if (!comparePositions(nextPosition, apple)) {
newSnake.shift();
}
/* Check if the snake hits itself */
if (newSnake.slice(0, -1).filter(snakePart => comparePositions(snakePart, nextPosition)).length) {
if (!allowWrapping && isOutOfBound(nextPosition, size)) {
newSnake = snake;
started = false;
died = true;
}
} else {
newSnake = [...snake, nextPosition];
if (comparePositions(nextPosition, apple)) {
apple = randomPosition(size);
// eslint-disable-next-line no-loop-func
while (newSnake.filter(snakePart => comparePositions(snakePart, apple)).length) {
/* If the snake did not eat an apple then remove the last part of its tail */
if (!comparePositions(nextPosition, apple)) {
newSnake.shift();
}
/* Check if the snake hits itself */
if (newSnake.slice(0, -1).filter(snakePart => comparePositions(snakePart, nextPosition)).length) {
started = false;
died = true;
}
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);
}
}
}
updateGrid({ grid, apple, snake: newSnake });
@@ -188,6 +222,8 @@ module.reducer(UPDATE_FRAME_SNAKE, state => {
score
};
});
module.reducer(CHANGE_ALLOW_WRAPPING, (state, { allowWrapping }) => ({ ...state, allowWrapping }));
module.reducer(TOGGLE_ALLOW_WRAPPING, state => ({ ...state, allowWrapping: !state.allowWrapping }));
/* === Middleware =============================================================================== */
@@ -259,6 +295,9 @@ module.middleware(KEY_PRESSED_SNAKE, (dispatch, _, { key, altKey, ctrlKey, metaK
if (key === keys.r) {
dispatch(startSnake());
}
if (key === keys.w) {
dispatch(toggleAllowWrapping());
}
/* Map the number keys to a zoom level */
const keyAsInt = parseInt(key, 10);

View File

@@ -2,11 +2,13 @@ import defaultTheme from "./themes/default.js";
import lightTheme from "./themes/light.js";
import darkTheme from "./themes/dark.js";
import darkOceanTheme from "./themes/darkOcean.js";
import forestNightTheme from "./themes/forestNight.js";
const themes = {
default: defaultTheme,
light: lightTheme,
dark: darkTheme,
forestNight: forestNightTheme,
darkOcean: darkOceanTheme
};

View File

@@ -18,7 +18,10 @@ export const colors = {
spinnerHighlight: "#db7093",
cardFoldHighlight: "#ad5a75",
cardFoldShadow: "#bdb19a",
selectColor: "#db7093"
selectColor: "#db7093",
pageSectionMainColor: "#333",
pageSectionStandardColor: "#444",
pageSectionAlternateColor: "#555"
};
const mapRange = (l, i, min, max) => Math.round((max - min) * ((l - i) / l) + min);

View File

@@ -3,11 +3,11 @@ import { renderTheme } from "./default.js";
export const colors = {
background: "#333",
backgroundActive: "#828282",
backgroundActive: "transparent",
backgroundInactive: "#4d4d4d",
backgroundAlternate: "#555",
color: "#7094db",
colorActive: "#ecb1c5",
colorActive: "#7094db",
colorInactive: "#7d98d4",
colorAlternate: "#a1c0fc",
shadowColor: "#222",
@@ -18,7 +18,10 @@ export const colors = {
spinnerHighlight: "#db7093",
cardFoldHighlight: "#091225",
cardFoldShadow: "#bdb19a",
selectColor: "#7094db"
selectColor: "#7094db",
pageSectionMainColor: "#333",
pageSectionStandardColor: "#444",
pageSectionAlternateColor: "#555"
};
const mapRange = (l, i, min, max) => Math.round((max - min) * ((l - i) / l) + min);

View File

@@ -64,6 +64,14 @@ export const renderTheme = themeColors => {
}
}
},
checkbox: {
borderWidth: `${2 / 16}rem`,
borderRadius: `${3 / 16}rem`,
borderColor: colors.color,
checked: {
background: colors.colorAlternate
}
},
button: {
borderColor: colors.borderColor,
background: colors.color,

View File

@@ -0,0 +1,52 @@
import merge from "deepmerge";
import { renderTheme } from "./default.js";
export const colors = {
background: "#333",
backgroundActive: "#828282",
backgroundInactive: "#4d4d4d",
backgroundAlternate: "#555",
// color: "#84db70",
color: "#74EA4D",
colorActive: "#ecb1c5",
colorInactive: "#addda2",
colorAlternate: "#55af41",
shadowColor: "#222",
borderColor: "#222",
borderColorActive: "silver",
borderColorInactive: "#addda2",
spinnerShadow: "#444",
spinnerHighlight: "#db7093",
cardFoldHighlight: "#091225",
cardFoldShadow: "#bdb19a",
selectColor: "#addda2",
pageSectionMainColor: "#333",
pageSectionStandardColor: "#444",
pageSectionAlternateColor: "#555"
};
const mapRange = (l, i, min, max) => Math.round((max - min) * ((l - i) / l) + min);
export const darkTheme = merge(renderTheme(colors), {
snakePart: {
boxShadow: "1px 1px 2px #888 inset, -1px -1px 2px #222 inset",
getColor(length, index, died) {
const hue = 109;
if (!died) {
const saturation = mapRange(length, index, 20, 65);
const brightness = 61;
return `hsl(${hue}, ${saturation}%, ${brightness}%)`;
}
const saturation = 0;
const brightness = mapRange(length, index, 70, 0);
return `hsl(${hue}, ${saturation}%, ${brightness}%)`;
}
},
stage: {
cell: {
boxShadow: "1px 1px 3px #191919 inset, -1px -1px 1px #777777 inset"
}
}
});
export default darkTheme;

View File

@@ -17,6 +17,15 @@ module.exports = {
port: 9000,
historyApiFallback: true
},
externals: {
react: "React",
React: "React",
redux: "Redux",
"react-dom": "ReactDOM",
"react-redux": "ReactRedux",
"react-router": "ReactRouter"
// "react-router-dom": "ReactRouterDOM"
},
plugins: [new webpack.HotModuleReplacementPlugin()],
module: {
rules: [

View File

@@ -1,4 +1,3 @@
const webpack = require("webpack");
const path = require("path");
module.exports = {
@@ -6,10 +5,15 @@ module.exports = {
mode: "production",
entry: path.resolve(__dirname, "src/index.js"),
output: { path: path.resolve(__dirname, "public") },
plugins: [new webpack.HotModuleReplacementPlugin()],
devServer: {
hot: false,
inline: false
externals: {
react: "React",
React: "React",
redux: "Redux",
"react-dom": "ReactDOM",
"react-redux": "ReactRedux",
"react-router": "ReactRouter",
"styled-components": "styled"
// "react-router-dom": "ReactRouterDOM"
},
module: {
rules: [