Update about page and documentation. Add allow wrapping option.
25
README.md
@@ -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.
|
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
|
## Feature Technologies
|
||||||
|
|
||||||
- React + React Router
|
- React
|
||||||
|
- React Router
|
||||||
- Redux
|
- Redux
|
||||||
- Styled Components
|
- Styled Components
|
||||||
- Webpack + Babel
|
- Webpack
|
||||||
|
- Babel
|
||||||
|
- Font Awesome
|
||||||
- Jest
|
- Jest
|
||||||
- ESLint
|
- ESLint
|
||||||
- pre-commit
|
- pre-commit
|
||||||
|
- Docker
|
||||||
|
|
||||||
## Todo
|
## TODO
|
||||||
|
|
||||||
[ ] Create welcome / instructions page / about page
|
[ ] Make stage wrapping an optional feature
|
||||||
[ ] Link to repository
|
|
||||||
[x] Add license
|
|
||||||
[x] High score
|
|
||||||
[ ] Docker build script
|
|
||||||
[ ] Improve banner / popup windows
|
|
||||||
[ ] Touch and mobile support
|
[ ] Touch and mobile support
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
```
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) Crafity VOF <info@crafity.com> (crafity.com)
|
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,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
|
```
|
||||||
|
|||||||
@@ -5,8 +5,15 @@ server {
|
|||||||
#charset koi8-r;
|
#charset koi8-r;
|
||||||
#access_log /var/log/nginx/host.access.log main;
|
#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 / {
|
location / {
|
||||||
alias /usr/share/nginx/html/;
|
|
||||||
try_files $uri /index.html;
|
try_files $uri /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "webpack-dev-server --mode development --open",
|
"dev": "webpack-dev-server --mode development",
|
||||||
"build": "webpack --optimize-minimize --config webpack.production.config.js",
|
"build": "webpack --optimize-minimize --config webpack.production.config.js",
|
||||||
"webpack": "webpack --config webpack.config.js --watch",
|
"webpack": "webpack --config webpack.config.js --watch",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
"react-redux": "^7.1.0",
|
"react-redux": "^7.1.0",
|
||||||
"react-router": "^5.0.1",
|
"react-router": "^5.0.1",
|
||||||
"react-router-dom": "^5.0.1",
|
"react-router-dom": "^5.0.1",
|
||||||
|
"react-router-hash-link": "^1.2.2",
|
||||||
"react-transition-group": "^4.2.2",
|
"react-transition-group": "^4.2.2",
|
||||||
"redux": "^4.0.4",
|
"redux": "^4.0.4",
|
||||||
"styled-components": "^4.3.2"
|
"styled-components": "^4.3.2"
|
||||||
|
|||||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
public/img/docker.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/img/font-awesome.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
public/img/jest.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
public/img/reactjs.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/img/reduxjs.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
public/img/snake.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/img/styled-components.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
public/img/webpack.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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 href="https://fonts.googleapis.com/css?family=Lato:100,400|Raleway:100,400,700|Rubik:500" rel="stylesheet" />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
@@ -16,6 +16,13 @@
|
|||||||
integrity="sha384-NnhYAEceBbm5rQuNvCv6o4iIoPZlkaWfvuXVh4XkRNvHWKgu/Mk2nEjFZpPQdwiz"
|
integrity="sha384-NnhYAEceBbm5rQuNvCv6o4iIoPZlkaWfvuXVh4XkRNvHWKgu/Mk2nEjFZpPQdwiz"
|
||||||
crossorigin="anonymous"
|
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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
49
src/components/Checkbox.js
Normal 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;
|
||||||
@@ -2,10 +2,12 @@ import styled from "styled-components";
|
|||||||
|
|
||||||
export const Code = styled.code`
|
export const Code = styled.code`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: white;
|
font-weight: bold;
|
||||||
background-color: gray;
|
background-color: ${({ theme }) => theme.button.background};
|
||||||
padding: 0 0.2rem;
|
color: ${({ theme }) => theme.button.color};
|
||||||
line-height: 1.4rem;
|
padding: 0 0.3rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
border-radius: 0.125rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default Code;
|
export default Code;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { IconButton, ToggleIconButton } from "../components/Button.js";
|
import { IconButton, ToggleIconButton } from "../components/Button.js";
|
||||||
|
import Link from "../components/Link.js";
|
||||||
|
import Checkbox from "../components/Checkbox.js";
|
||||||
|
|
||||||
const buttonSize = 2.5;
|
const buttonSize = 2.5;
|
||||||
|
|
||||||
@@ -33,9 +35,26 @@ export const ControlPanel = ({
|
|||||||
zoom,
|
zoom,
|
||||||
setFps,
|
setFps,
|
||||||
zoomIn,
|
zoomIn,
|
||||||
zoomOut
|
zoomOut,
|
||||||
|
allowWrapping,
|
||||||
|
changeAllowWrapping
|
||||||
}) => (
|
}) => (
|
||||||
<Layout>
|
<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" }}>
|
<HorizontalStack style={{ minHeight: "2rem", lineHeight: "2rem" }}>
|
||||||
<span>
|
<span>
|
||||||
Zoom: <b>{zoom}</b>
|
Zoom: <b>{zoom}</b>
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ const HighscoreEntry = styled.li`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Highscore = ({ highscores = [] }) => {
|
const Highscore = ({ highscores = [] }) => {
|
||||||
console.log("highscores", highscores);
|
|
||||||
return (
|
return (
|
||||||
<HighscoreContainer>
|
<HighscoreContainer>
|
||||||
<Title>Highscore</Title>
|
<Title>Highscore</Title>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { NavLink } from "react-router-dom";
|
import { NavLink } from "react-router-dom";
|
||||||
|
import { HashLink } from "react-router-hash-link";
|
||||||
|
|
||||||
const SharedStyle = () => `
|
const SharedStyle = () => `
|
||||||
color: gray;
|
color: gray;
|
||||||
@@ -19,6 +20,13 @@ const StyledNavLink = styled(NavLink)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledHashLink = styled(HashLink)`
|
||||||
|
${SharedStyle}
|
||||||
|
&.active {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const Link = (props, refs) => {
|
const Link = (props, refs) => {
|
||||||
const updatedProps = { ...props };
|
const updatedProps = { ...props };
|
||||||
|
|
||||||
@@ -26,7 +34,11 @@ const Link = (props, refs) => {
|
|||||||
updatedProps.onClick = e => e.preventDefault() || props.onClick();
|
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;
|
export default Link;
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ export const Content = styled.div`
|
|||||||
content === PageSection.content.center &&
|
content === PageSection.content.center &&
|
||||||
css`
|
css`
|
||||||
max-width: 60rem;
|
max-width: 60rem;
|
||||||
|
min-height: 31.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
// padding: 0.5em;
|
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -16,10 +19,12 @@ const contentPosition = {
|
|||||||
stretch: "stretch"
|
stretch: "stretch"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageSection = ({ content, children, className } = { content: contentPosition.stretch }) => {
|
export const PageSection = ({ content, children, className, center, stretch } = {}) => {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<Content content={content}>{children}</Content>
|
<Content content={content || (center && contentPosition.center) || (stretch && contentPosition.stretch)}>
|
||||||
|
<div>{children}</div>
|
||||||
|
</Content>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ const Cell = styled.div.attrs(({ theme, zoom }) => ({
|
|||||||
box-shadow: ${({ theme }) => theme.stage.cell.boxShadow};
|
box-shadow: ${({ theme }) => theme.stage.cell.boxShadow};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Stage = ({ data, children, zoom = 1 }) => (
|
export const Stage = ({ grid, children, zoom = 1 }) => (
|
||||||
<div>
|
<div>
|
||||||
{data.map((r, y) => (
|
{grid.map((r, y) => (
|
||||||
<Row key={`y${y}`} zoom={zoom}>
|
<Row key={`y${y}`} {...{ zoom }}>
|
||||||
{r.map((c, x) => (
|
{r.map((c, x) => (
|
||||||
<Cell key={`x${x}y${y}`} zoom={zoom}>
|
<Cell key={`x${x}y${y}`} {...{ zoom }}>
|
||||||
{children(c, zoom)}
|
{children(c, zoom)}
|
||||||
</Cell>
|
</Cell>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const ThemeSelector = ({ theme, changeTheme, border }) => {
|
|||||||
<StyledSelect border={border} onChange={e => changeTheme(e.target.value)} value={theme}>
|
<StyledSelect border={border} onChange={e => changeTheme(e.target.value)} value={theme}>
|
||||||
<option value="light">Light</option>
|
<option value="light">Light</option>
|
||||||
<option value="dark">Miami Night</option>
|
<option value="dark">Miami Night</option>
|
||||||
|
<option value="forestNight">Forest Night</option>
|
||||||
<option value="darkOcean">Dark Ocean</option>
|
<option value="darkOcean">Dark Ocean</option>
|
||||||
<option value="default">Black / White</option>
|
<option value="default">Black / White</option>
|
||||||
</StyledSelect>
|
</StyledSelect>
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import styled, { css } from "styled-components";
|
import styled, { css } from "styled-components";
|
||||||
|
|
||||||
const Title = styled.h1`
|
const Title = styled.h1.attrs(({ id }) => ({
|
||||||
|
id
|
||||||
|
}))`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: 4rem;
|
line-height: 4rem;
|
||||||
|
|
||||||
|
${props =>
|
||||||
|
props.smaller &&
|
||||||
|
css`
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 3rem;
|
||||||
|
`}
|
||||||
|
|
||||||
${props =>
|
${props =>
|
||||||
props.large &&
|
props.large &&
|
||||||
css`
|
css`
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import styled, { css } from "styled-components";
|
|||||||
import Title from "../components/Title.js";
|
import Title from "../components/Title.js";
|
||||||
import PageSection from "../components/PageSection.js";
|
import PageSection from "../components/PageSection.js";
|
||||||
import Code from "../components/Code.js";
|
import Code from "../components/Code.js";
|
||||||
|
import Button from "../components/Button.js";
|
||||||
|
import Link from "../components/Link.js";
|
||||||
|
|
||||||
const MainPageSection = styled(PageSection)`
|
const MainPageSection = styled(PageSection)`
|
||||||
background-color: ${({ theme }) => theme.colors.pageSectionMainColor};
|
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 = () => (
|
export const About = () => (
|
||||||
<React.Fragment>
|
<StyledAboutPage>
|
||||||
<MainPageSection content={PageSection.content.center}>
|
<MainPageSection center>
|
||||||
<Layout style={{ justifyContent: "center" }} direction={"horizontal"}>
|
<Layout style={{ justifyContent: "center" }} direction={"horizontal"}>
|
||||||
<Layout style={{ width: "30rem", placeContent: "center" }} direction={"vertical"}>
|
<Layout style={{ width: "30rem", placeContent: "center" }} direction={"vertical"}>
|
||||||
<Title larger>Crafity Snake</Title>
|
<Title larger>Crafity Snake</Title>
|
||||||
@@ -65,58 +90,204 @@ export const About = () => (
|
|||||||
<Icon>🐍</Icon>
|
<Icon>🐍</Icon>
|
||||||
</Layout>
|
</Layout>
|
||||||
</MainPageSection>
|
</MainPageSection>
|
||||||
<StandardPageSection content={PageSection.content.center}>
|
<StandardPageSection center>
|
||||||
<Title>Introduction</Title>
|
<Title>Introduction</Title>
|
||||||
<p>
|
<p>
|
||||||
This experiment has been created to test out React, Redux and Styled Components and see if it is possible to
|
This game has been created as a <b>Crafity</b> experiment to test out React, Redux and Styled Components and see
|
||||||
create a simple web based game like snake. The source code for this game is open source and free to download and
|
if it is possible to create a simple web based game like snake. The{" "}
|
||||||
change.
|
<Link smooth to="/about#source-code">
|
||||||
|
source code
|
||||||
|
</Link>{" "}
|
||||||
|
for this game is open source and free to download and change.
|
||||||
</p>
|
</p>
|
||||||
<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.
|
reloaded or reopened any time and the game should continue from where it was left.
|
||||||
</p>
|
</p>
|
||||||
</StandardPageSection>
|
</StandardPageSection>
|
||||||
<AlternatePageSection content={PageSection.content.center}>
|
<AlternatePageSection center>
|
||||||
<Title>How to play</Title>
|
<Title id="how-to-play">How to play</Title>
|
||||||
<p>
|
<p>
|
||||||
You control the snake using the arrow keys or the <Code>hjkl</Code> keys. Everytime the snake eats an apple the
|
In this game you play a snake and the goal is to eat as many apples as possible. Every time your snake eats an
|
||||||
tail of the snake grows longer. When the snake touches his own body the game is over.
|
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>
|
||||||
<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
|
<i>* Only when allow wrapping is turned off</i>
|
||||||
and <Code>p</Code> to pause the game.
|
</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>
|
</p>
|
||||||
</AlternatePageSection>
|
</AlternatePageSection>
|
||||||
<StandardPageSection content={PageSection.content.center}>
|
<StandardPageSection center>
|
||||||
<Title>Technology</Title>
|
<Title>Technology</Title>
|
||||||
<p>Snake has been developed with the folowing technologies.</p>
|
<p>Snake has been developed with the folowing technologies.</p>
|
||||||
<ul>
|
<Layout style={{ justifyContent: "space-around" }}>
|
||||||
<li>React + React Router</li>
|
<ul>
|
||||||
<li>Redux</li>
|
<li>React</li>
|
||||||
<li>Styled Components</li>
|
<li>React Router</li>
|
||||||
<li>Webpack + Babel</li>
|
<li>Redux</li>
|
||||||
<li>Jest</li>
|
<li>Styled Components</li>
|
||||||
<li>ESLint</li>
|
<li>Webpack</li>
|
||||||
<li>pre-commit</li>
|
<li>Babel</li>
|
||||||
<li>Docker</li>
|
</ul>
|
||||||
</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"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"s you style
|
||||||
|
your react components directly from your Javascript code.
|
||||||
|
</p>
|
||||||
</StandardPageSection>
|
</StandardPageSection>
|
||||||
<AlternatePageSection content={PageSection.content.center}>
|
<AlternatePageSection center>
|
||||||
<Title>Source Code</Title>
|
<Title id="source-code">Source Code</Title>
|
||||||
<p>The source code is hosted on Crafity's git repositories at the following location:</p>
|
<p>The source code is hosted on Crafity's Gitea environment at the following location:</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://git.crafity.com">Crafity Snake</a>
|
<Link target="_blank" href="https://git.crafity.com/Crafity/snake">
|
||||||
</p>
|
Download here
|
||||||
<p>After donwloading the source code run the following commands:</p>
|
</Link>
|
||||||
<p>
|
|
||||||
<Code>npm install</Code>
|
|
||||||
</p>
|
</p>
|
||||||
<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"s git repository to file issues and pull requests.
|
||||||
</p>
|
</p>
|
||||||
</AlternatePageSection>
|
</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;
|
export default About;
|
||||||
|
|||||||
7
src/containers/Empty.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const Empty = () => {
|
||||||
|
return <h1>lskdfj</h1>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Empty;
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
pauseSnake,
|
pauseSnake,
|
||||||
updateFrameSnake,
|
updateFrameSnake,
|
||||||
keyPressedSnake,
|
keyPressedSnake,
|
||||||
|
changeAllowWrapping,
|
||||||
zoomIn,
|
zoomIn,
|
||||||
zoomOut
|
zoomOut
|
||||||
} from "../redux/snake.js";
|
} from "../redux/snake.js";
|
||||||
@@ -68,7 +69,9 @@ const Snake = ({
|
|||||||
lastGameId,
|
lastGameId,
|
||||||
zoom,
|
zoom,
|
||||||
zoomIn,
|
zoomIn,
|
||||||
zoomOut
|
zoomOut,
|
||||||
|
allowWrapping,
|
||||||
|
changeAllowWrapping
|
||||||
}) => {
|
}) => {
|
||||||
const onSubmitHighscore = result => {
|
const onSubmitHighscore = result => {
|
||||||
if (result) {
|
if (result) {
|
||||||
@@ -82,10 +85,10 @@ const Snake = ({
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Layout>
|
<Layout>
|
||||||
<StageContainer>
|
<StageContainer>
|
||||||
<Stage data={grid} zoom={zoom}>
|
<Stage {...{ zoom, grid }}>
|
||||||
{cell =>
|
{cell =>
|
||||||
(cell.type === "apple" && <Apple zoom={zoom} died={died} />) ||
|
(cell.type === "apple" && <Apple {...{ zoom, died }} />) ||
|
||||||
(cell.type === "snake" && <SnakePart zoom={zoom} value={cell} died={died} />)
|
(cell.type === "snake" && <SnakePart value={cell} {...{ zoom, died }} />)
|
||||||
}
|
}
|
||||||
</Stage>
|
</Stage>
|
||||||
{!started && (
|
{!started && (
|
||||||
@@ -101,26 +104,30 @@ const Snake = ({
|
|||||||
</Banner>
|
</Banner>
|
||||||
)}
|
)}
|
||||||
{died && lastGameId !== gameId && hasHighscore(score) && (
|
{died && lastGameId !== gameId && hasHighscore(score) && (
|
||||||
<HighscoreInput score={score} gameId={gameId} name={lastUsername} onSubmit={onSubmitHighscore} />
|
<HighscoreInput name={lastUsername} onSubmit={onSubmitHighscore} {...{ score, gameId }} />
|
||||||
)}
|
)}
|
||||||
</StageContainer>
|
</StageContainer>
|
||||||
<SidePanel>
|
<SidePanel>
|
||||||
<Scoreboard score={score} zoom={3} />
|
<Scoreboard {...{ score }} zoom={3} />
|
||||||
<Highscore />
|
<Highscore />
|
||||||
<ControlPanel
|
<ControlPanel
|
||||||
updateFrameSnake={updateFrameSnake}
|
{...{
|
||||||
started={started}
|
updateFrameSnake,
|
||||||
paused={paused}
|
started,
|
||||||
pauseSnake={pauseSnake}
|
paused,
|
||||||
stopSnake={stopSnake}
|
pauseSnake,
|
||||||
startSnake={startSnake}
|
stopSnake,
|
||||||
theme={theme}
|
startSnake,
|
||||||
changeTheme={changeTheme}
|
theme,
|
||||||
fps={fps}
|
changeTheme,
|
||||||
setFps={setFps}
|
fps,
|
||||||
zoom={zoom}
|
setFps,
|
||||||
zoomIn={zoomIn}
|
zoom,
|
||||||
zoomOut={zoomOut}
|
zoomIn,
|
||||||
|
zoomOut,
|
||||||
|
allowWrapping,
|
||||||
|
changeAllowWrapping
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</SidePanel>
|
</SidePanel>
|
||||||
</Layout>
|
</Layout>
|
||||||
@@ -148,7 +155,8 @@ const mapActionsToProps = {
|
|||||||
zoomOut,
|
zoomOut,
|
||||||
changeTheme,
|
changeTheme,
|
||||||
registerHighscore,
|
registerHighscore,
|
||||||
skipHighscore
|
skipHighscore,
|
||||||
|
changeAllowWrapping
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const keys = {
|
|||||||
p: "p",
|
p: "p",
|
||||||
s: "s",
|
s: "s",
|
||||||
r: "r",
|
r: "r",
|
||||||
|
w: "w",
|
||||||
|
|
||||||
1: "1",
|
1: "1",
|
||||||
2: "2",
|
2: "2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { Provider } from "react-redux";
|
import { Provider, ReactReduxContext } from "react-redux";
|
||||||
import { ConnectedRouter } from "connected-react-router";
|
import { ConnectedRouter } from "connected-react-router";
|
||||||
|
|
||||||
import { createStore } from "./redux/store";
|
import { createStore } from "./redux/store";
|
||||||
@@ -13,8 +13,8 @@ const store = createStore();
|
|||||||
window.store = store;
|
window.store = store;
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={store}>
|
<Provider store={store} context={ReactReduxContext}>
|
||||||
<ConnectedRouter history={store.history}>
|
<ConnectedRouter history={store.history} context={ReactReduxContext}>
|
||||||
<App />
|
<App />
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
|
|||||||
@@ -214,7 +214,6 @@ export class Module {
|
|||||||
|
|
||||||
select(...items) {
|
select(...items) {
|
||||||
return (items || []).reduce((selection, item) => {
|
return (items || []).reduce((selection, item) => {
|
||||||
console.log("item", item);
|
|
||||||
return selection;
|
return selection;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const DEFAUL_ZOOM_STEP = 0.1;
|
|||||||
const initialState = {
|
const initialState = {
|
||||||
grid: createGrid(DEFAULT_GRID_SIZE),
|
grid: createGrid(DEFAULT_GRID_SIZE),
|
||||||
zoom: 1.5,
|
zoom: 1.5,
|
||||||
|
allowWrapping: false,
|
||||||
size: DEFAULT_GRID_SIZE,
|
size: DEFAULT_GRID_SIZE,
|
||||||
paused: false,
|
paused: false,
|
||||||
gameId: 0,
|
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 newX = x + vX;
|
||||||
let newY = y + vY;
|
let newY = y + vY;
|
||||||
|
if (!allowWrapping) {
|
||||||
|
return [newX, newY];
|
||||||
|
}
|
||||||
if (newX > size - 1) {
|
if (newX > size - 1) {
|
||||||
newX = 0;
|
newX = 0;
|
||||||
}
|
}
|
||||||
@@ -82,6 +86,22 @@ const moveSnakePart = ([x, y], vX, vY, size) => {
|
|||||||
return [newX, newY];
|
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);
|
export const module = new Module(MODULE_NAME, initialState);
|
||||||
|
|
||||||
/* === Actions ================================================================================== */
|
/* === 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 [UPDATE_FRAME_SNAKE, updateFrameSnake] = module.action("UPDATE_FRAME_SNAKE");
|
||||||
export const [CHANGE_DIRECTION, changeDirection] = module.action("CHANGE_DIRECTION", direction => ({ direction }));
|
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 [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) => ({
|
export const [UPDATE_STATE, updateState] = module.action("UPDATE_STATE", (newState, fullUpdate = false) => ({
|
||||||
newState,
|
newState,
|
||||||
fullUpdate
|
fullUpdate
|
||||||
@@ -117,11 +142,12 @@ module.reducer(ZOOM_OUT, (state, { step = DEFAUL_ZOOM_STEP }) =>
|
|||||||
module.reducer(RESET_SNAKE, (state, options) => {
|
module.reducer(RESET_SNAKE, (state, options) => {
|
||||||
const grid = createGrid(state.size);
|
const grid = createGrid(state.size);
|
||||||
const zoom = state.zoom;
|
const zoom = state.zoom;
|
||||||
|
const allowWrapping = state.allowWrapping;
|
||||||
const gameId = (state.gameId || 0) + 1;
|
const gameId = (state.gameId || 0) + 1;
|
||||||
const apple = options.started ? randomPosition(state.size) : initialState.apple;
|
const apple = options.started ? randomPosition(state.size) : initialState.apple;
|
||||||
const snake = options.started ? [[0, 0]] : initialState.snake;
|
const snake = options.started ? [[0, 0]] : initialState.snake;
|
||||||
updateGrid({ grid, apple, 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 }) => {
|
module.reducer(CHANGE_DIRECTION, (state, { direction }) => {
|
||||||
@@ -145,31 +171,39 @@ module.reducer(CHANGE_DIRECTION, (state, { direction }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
module.reducer(UPDATE_FRAME_SNAKE, state => {
|
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;
|
let { apple, started, died, score } = state;
|
||||||
const grid = createGrid(size);
|
const grid = createGrid(size);
|
||||||
|
|
||||||
const [vXNext, vYNext] = next.length ? next[0] : [vX, vY];
|
const [vXNext, vYNext] = next.length ? next[0] : [vX, vY];
|
||||||
|
|
||||||
const nextPosition = moveSnakePart(snake[snake.length - 1], vXNext, vYNext, size);
|
const nextPosition = moveSnakePart(snake[snake.length - 1], vXNext, vYNext, size, allowWrapping);
|
||||||
const newSnake = [...snake, nextPosition];
|
let newSnake;
|
||||||
|
|
||||||
/* If the snake did not eat an apple then remove the last part of its tail */
|
if (!allowWrapping && isOutOfBound(nextPosition, size)) {
|
||||||
if (!comparePositions(nextPosition, apple)) {
|
newSnake = snake;
|
||||||
newSnake.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Check if the snake hits itself */
|
|
||||||
if (newSnake.slice(0, -1).filter(snakePart => comparePositions(snakePart, nextPosition)).length) {
|
|
||||||
started = false;
|
started = false;
|
||||||
died = true;
|
died = true;
|
||||||
}
|
} else {
|
||||||
|
newSnake = [...snake, nextPosition];
|
||||||
|
|
||||||
if (comparePositions(nextPosition, apple)) {
|
/* If the snake did not eat an apple then remove the last part of its tail */
|
||||||
apple = randomPosition(size);
|
if (!comparePositions(nextPosition, apple)) {
|
||||||
// eslint-disable-next-line no-loop-func
|
newSnake.shift();
|
||||||
while (newSnake.filter(snakePart => comparePositions(snakePart, apple)).length) {
|
}
|
||||||
|
|
||||||
|
/* 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);
|
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 });
|
updateGrid({ grid, apple, snake: newSnake });
|
||||||
@@ -188,6 +222,8 @@ module.reducer(UPDATE_FRAME_SNAKE, state => {
|
|||||||
score
|
score
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
module.reducer(CHANGE_ALLOW_WRAPPING, (state, { allowWrapping }) => ({ ...state, allowWrapping }));
|
||||||
|
module.reducer(TOGGLE_ALLOW_WRAPPING, state => ({ ...state, allowWrapping: !state.allowWrapping }));
|
||||||
|
|
||||||
/* === Middleware =============================================================================== */
|
/* === Middleware =============================================================================== */
|
||||||
|
|
||||||
@@ -259,6 +295,9 @@ module.middleware(KEY_PRESSED_SNAKE, (dispatch, _, { key, altKey, ctrlKey, metaK
|
|||||||
if (key === keys.r) {
|
if (key === keys.r) {
|
||||||
dispatch(startSnake());
|
dispatch(startSnake());
|
||||||
}
|
}
|
||||||
|
if (key === keys.w) {
|
||||||
|
dispatch(toggleAllowWrapping());
|
||||||
|
}
|
||||||
|
|
||||||
/* Map the number keys to a zoom level */
|
/* Map the number keys to a zoom level */
|
||||||
const keyAsInt = parseInt(key, 10);
|
const keyAsInt = parseInt(key, 10);
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import defaultTheme from "./themes/default.js";
|
|||||||
import lightTheme from "./themes/light.js";
|
import lightTheme from "./themes/light.js";
|
||||||
import darkTheme from "./themes/dark.js";
|
import darkTheme from "./themes/dark.js";
|
||||||
import darkOceanTheme from "./themes/darkOcean.js";
|
import darkOceanTheme from "./themes/darkOcean.js";
|
||||||
|
import forestNightTheme from "./themes/forestNight.js";
|
||||||
|
|
||||||
const themes = {
|
const themes = {
|
||||||
default: defaultTheme,
|
default: defaultTheme,
|
||||||
light: lightTheme,
|
light: lightTheme,
|
||||||
dark: darkTheme,
|
dark: darkTheme,
|
||||||
|
forestNight: forestNightTheme,
|
||||||
darkOcean: darkOceanTheme
|
darkOcean: darkOceanTheme
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ export const colors = {
|
|||||||
spinnerHighlight: "#db7093",
|
spinnerHighlight: "#db7093",
|
||||||
cardFoldHighlight: "#ad5a75",
|
cardFoldHighlight: "#ad5a75",
|
||||||
cardFoldShadow: "#bdb19a",
|
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);
|
const mapRange = (l, i, min, max) => Math.round((max - min) * ((l - i) / l) + min);
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { renderTheme } from "./default.js";
|
|||||||
|
|
||||||
export const colors = {
|
export const colors = {
|
||||||
background: "#333",
|
background: "#333",
|
||||||
backgroundActive: "#828282",
|
backgroundActive: "transparent",
|
||||||
backgroundInactive: "#4d4d4d",
|
backgroundInactive: "#4d4d4d",
|
||||||
backgroundAlternate: "#555",
|
backgroundAlternate: "#555",
|
||||||
color: "#7094db",
|
color: "#7094db",
|
||||||
colorActive: "#ecb1c5",
|
colorActive: "#7094db",
|
||||||
colorInactive: "#7d98d4",
|
colorInactive: "#7d98d4",
|
||||||
colorAlternate: "#a1c0fc",
|
colorAlternate: "#a1c0fc",
|
||||||
shadowColor: "#222",
|
shadowColor: "#222",
|
||||||
@@ -18,7 +18,10 @@ export const colors = {
|
|||||||
spinnerHighlight: "#db7093",
|
spinnerHighlight: "#db7093",
|
||||||
cardFoldHighlight: "#091225",
|
cardFoldHighlight: "#091225",
|
||||||
cardFoldShadow: "#bdb19a",
|
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);
|
const mapRange = (l, i, min, max) => Math.round((max - min) * ((l - i) / l) + min);
|
||||||
|
|||||||
@@ -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: {
|
button: {
|
||||||
borderColor: colors.borderColor,
|
borderColor: colors.borderColor,
|
||||||
background: colors.color,
|
background: colors.color,
|
||||||
|
|||||||
52
src/theming/themes/forestNight.js
Normal 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;
|
||||||
@@ -17,6 +17,15 @@ module.exports = {
|
|||||||
port: 9000,
|
port: 9000,
|
||||||
historyApiFallback: true
|
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()],
|
plugins: [new webpack.HotModuleReplacementPlugin()],
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
const webpack = require("webpack");
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -6,10 +5,15 @@ module.exports = {
|
|||||||
mode: "production",
|
mode: "production",
|
||||||
entry: path.resolve(__dirname, "src/index.js"),
|
entry: path.resolve(__dirname, "src/index.js"),
|
||||||
output: { path: path.resolve(__dirname, "public") },
|
output: { path: path.resolve(__dirname, "public") },
|
||||||
plugins: [new webpack.HotModuleReplacementPlugin()],
|
externals: {
|
||||||
devServer: {
|
react: "React",
|
||||||
hot: false,
|
React: "React",
|
||||||
inline: false
|
redux: "Redux",
|
||||||
|
"react-dom": "ReactDOM",
|
||||||
|
"react-redux": "ReactRedux",
|
||||||
|
"react-router": "ReactRouter",
|
||||||
|
"styled-components": "styled"
|
||||||
|
// "react-router-dom": "ReactRouterDOM"
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
|||||||