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.
|
||||
|
||||
## 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.
|
||||
```
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
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">
|
||||
<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>
|
||||
|
||||
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`
|
||||
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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -48,7 +48,6 @@ const HighscoreEntry = styled.li`
|
||||
`;
|
||||
|
||||
const Highscore = ({ highscores = [] }) => {
|
||||
console.log("highscores", highscores);
|
||||
return (
|
||||
<HighscoreContainer>
|
||||
<Title>Highscore</Title>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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"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>
|
||||
<AlternatePageSection content={PageSection.content.center}>
|
||||
<Title>Source Code</Title>
|
||||
<p>The source code is hosted on Crafity'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'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"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
@@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
export const Empty = () => {
|
||||
return <h1>lskdfj</h1>;
|
||||
};
|
||||
|
||||
export default Empty;
|
||||
@@ -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(
|
||||
|
||||
@@ -16,6 +16,7 @@ export const keys = {
|
||||
p: "p",
|
||||
s: "s",
|
||||
r: "r",
|
||||
w: "w",
|
||||
|
||||
1: "1",
|
||||
2: "2",
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -214,7 +214,6 @@ export class Module {
|
||||
|
||||
select(...items) {
|
||||
return (items || []).reduce((selection, item) => {
|
||||
console.log("item", item);
|
||||
return selection;
|
||||
}, {});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
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,
|
||||
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: [
|
||||
|
||||
@@ -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: [
|
||||
|
||||