Implemented snake scoreboard and working towards a dedicated snake app
This commit is contained in:
@@ -39,7 +39,7 @@ const StyledNavLink = styled(NavLink)`
|
||||
}
|
||||
`;
|
||||
|
||||
const Button = (props, refs) => {
|
||||
export const Button = (props, refs) => {
|
||||
const updatedProps = { ...props };
|
||||
|
||||
if (props.onClick) {
|
||||
@@ -57,4 +57,8 @@ const Button = (props, refs) => {
|
||||
return StyledButton.render(updatedProps);
|
||||
};
|
||||
|
||||
export const ToggleButton = styled(Button)`
|
||||
background-color: ${props => (props.toggle ? "silver" : null)};
|
||||
`;
|
||||
|
||||
export default Button;
|
||||
|
||||
@@ -12,7 +12,8 @@ const LogoPanel = styled.div`
|
||||
}
|
||||
|
||||
h1 {
|
||||
line-height: 2rem;
|
||||
line-height: 2.5rem;
|
||||
font-size: 2.3rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@@ -22,8 +23,8 @@ const LogoPanel = styled.div`
|
||||
|
||||
const Logo = () => (
|
||||
<LogoPanel>
|
||||
<h1>Hello Redux</h1>
|
||||
<h2>Redux demo application</h2>
|
||||
<h1>Crafity Snake</h1>
|
||||
<h2>React Redux Demo Application</h2>
|
||||
</LogoPanel>
|
||||
);
|
||||
|
||||
|
||||
158
src/components/Scoreboard.js
Normal file
158
src/components/Scoreboard.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
||||
|
||||
const DEFAULT_WIDTH = 1;
|
||||
const DEFAULT_HEIGHT = 1.5;
|
||||
|
||||
const Card = styled.div`
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
margin-right: 0.2em;
|
||||
width: ${props => (props.zoom || 1) * DEFAULT_WIDTH}rem;
|
||||
height: ${props => (props.zoom || 1) * DEFAULT_HEIGHT}rem;
|
||||
content: "${props => props.value}";
|
||||
border: 1px solid gray;
|
||||
border-radius: 1px;
|
||||
text-align: center;
|
||||
line-height: ${props => (props.zoom || 1) * (DEFAULT_HEIGHT + 0)}rem;
|
||||
vertical-align: middle;
|
||||
background-color: ${props => props.theme.body.color};
|
||||
color: ${props => props.theme.body.background};
|
||||
font-size: ${props => (props.zoom || 1) * 1.5}rem;
|
||||
font-family: Rubik,monospace;
|
||||
font-weight: 500;
|
||||
box-shadow: 1px 1px 3px #d0bfa3;
|
||||
text-shadow: 0 0 1px black;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
border-bottom: solid 2px #bdb19a;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
border-bottom: dotted 2px #ad5a75;
|
||||
z-index:1;
|
||||
}
|
||||
|
||||
// &::after {
|
||||
// content: "${props => props.children}";
|
||||
// display: block;
|
||||
// position: absolute;
|
||||
// font-size: 3em;
|
||||
// }
|
||||
`;
|
||||
|
||||
const StyledScoreboard = styled.div`
|
||||
height: ${props => (props.zoom || 1) * DEFAULT_HEIGHT}rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
& > div {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: ${props => (props.zoom || 1) * DEFAULT_WIDTH}rem;
|
||||
height: ${props => (props.zoom || 1) * DEFAULT_HEIGHT}rem;
|
||||
margin-right: ${props => (props.zoom || 1) * 0.3}rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const CardCSSTransition = styled(CSSTransition)`
|
||||
position: absolute;
|
||||
|
||||
&.card-enter,
|
||||
&.card-appear {
|
||||
${Card} {
|
||||
transform: rotateX(90deg);
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.card-appear-active {
|
||||
${Card} {
|
||||
transition: transform ease-out 250ms, opacity ease-out 100ms;
|
||||
transform: rotateX(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.card-enter-active {
|
||||
${Card} {
|
||||
transition: transform ease-out 250ms 250ms, opacity ease-out 100ms;
|
||||
transform: rotateX(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.card-exit {
|
||||
${Card} {
|
||||
transform: rotateX(0deg);
|
||||
transition: transform ease-out 250ms, opacity ease-out 100ms 150ms;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.card-exit-active {
|
||||
${Card} {
|
||||
transition: transform ease-out 250ms, opacity ease-out 100ms 150ms;
|
||||
transform: rotateX(90deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.card-exit-done {
|
||||
${Card} {
|
||||
transform: rotateX(90deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const Scoreboard = ({ score, zoom = 1 } = {}) => {
|
||||
const scoreCards = Array.from(`${score}`.padStart(3, "0"));
|
||||
return (
|
||||
<StyledScoreboard zoom={zoom}>
|
||||
<TransitionGroup>
|
||||
<CardCSSTransition key={`card0-${scoreCards[0] || "x"}`} timeout={1000} classNames="card">
|
||||
<div>
|
||||
<Card value={scoreCards[0]} zoom={zoom}>
|
||||
{scoreCards[0] || "0"}
|
||||
</Card>
|
||||
</div>
|
||||
</CardCSSTransition>
|
||||
</TransitionGroup>
|
||||
<TransitionGroup>
|
||||
<CardCSSTransition key={`card1-${scoreCards[1] || "x"}`} timeout={1000} classNames="card">
|
||||
<div>
|
||||
<Card value={scoreCards[1]} zoom={zoom}>
|
||||
{scoreCards[1] || "0"}
|
||||
</Card>
|
||||
</div>
|
||||
</CardCSSTransition>
|
||||
</TransitionGroup>
|
||||
<TransitionGroup>
|
||||
<CardCSSTransition key={`card2-${scoreCards[2] || "x"}`} timeout={1000} classNames="card">
|
||||
<div>
|
||||
<Card value={scoreCards[2]} zoom={zoom}>
|
||||
{scoreCards[2]}
|
||||
</Card>
|
||||
</div>
|
||||
</CardCSSTransition>
|
||||
</TransitionGroup>
|
||||
</StyledScoreboard>
|
||||
);
|
||||
};
|
||||
|
||||
export default Scoreboard;
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import styled, { keyframes, css } from "styled-components";
|
||||
import styled from "styled-components";
|
||||
|
||||
/* Redux Modules */
|
||||
import { module as gameModule, keyPress } from "../redux/game.js";
|
||||
import {
|
||||
module as snakeModule,
|
||||
@@ -11,12 +13,11 @@ import {
|
||||
keyPressedSnake,
|
||||
keys
|
||||
} from "../redux/snake.js";
|
||||
import Title from "../components/Title.js";
|
||||
import Button from "../components/Button.js";
|
||||
|
||||
const ToggleButton = styled(Button)`
|
||||
background-color: ${props => (props.toggle ? "silver" : null)};
|
||||
`;
|
||||
/* Components */
|
||||
import Title from "../components/Title.js";
|
||||
import Button, { ToggleButton } from "../components/Button.js";
|
||||
import Scoreboard from "../components/Scoreboard.js";
|
||||
|
||||
const Row = styled.div``;
|
||||
|
||||
@@ -68,82 +69,6 @@ const Skull = styled.div.attrs({ role: "img", "aria-label": "skull" })`
|
||||
}
|
||||
`;
|
||||
|
||||
const flipIn = keyframes`
|
||||
50% {
|
||||
transform: rotateX(90deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(0deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const flipOut = keyframes`
|
||||
0 {
|
||||
transform: rotateX(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotateX(90deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const Card = styled.div`
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
margin-right: 0.2em;
|
||||
width: 1rem;
|
||||
height: 2rem;
|
||||
// perspective: -1rem;
|
||||
|
||||
&:before {
|
||||
content: "${props => props.oldValue}";
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
// display: none;
|
||||
border: 1px solid gray;
|
||||
width: 1rem;
|
||||
height: 2rem;
|
||||
left: 0;
|
||||
top: 0;
|
||||
text-align: center;
|
||||
line-height: 2rem;
|
||||
vertical-align: middle;
|
||||
background-color: brown;
|
||||
color: white;
|
||||
transition: transform ease-out 100ms;
|
||||
// animation: ${flipIn} 1s infinite;
|
||||
${props =>
|
||||
props.value !== props.oldValue &&
|
||||
css`
|
||||
animation: ${flipIn} 1s forward;
|
||||
`};
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "${props => props.value}";
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
border: 1px solid gray;
|
||||
width: 1rem;
|
||||
height: 2rem;
|
||||
left: 0;
|
||||
top: 0;
|
||||
text-align: center;
|
||||
line-height: 2rem;
|
||||
vertical-align: middle;
|
||||
background-color: brown;
|
||||
color: white;
|
||||
transition: transform ease-out 100ms;
|
||||
// animation: ${flipOut} 1s infinite;
|
||||
${props =>
|
||||
props.value !== props.oldValue &&
|
||||
css`
|
||||
animation: ${flipOut} 1s forward;
|
||||
`};
|
||||
}
|
||||
|
||||
|
||||
`;
|
||||
|
||||
const Grid = ({ data, died }) =>
|
||||
data.map((r, y) => (
|
||||
<Row key={`y${y}`}>
|
||||
@@ -157,79 +82,6 @@ const Grid = ({ data, died }) =>
|
||||
</Row>
|
||||
));
|
||||
|
||||
const Score = ({ score, previousScore }) => {
|
||||
const now = Date.now();
|
||||
const [lastChange, changeLastChange] = React.useState({ timestamp: Date.now(), score, previousScore });
|
||||
|
||||
if (score === lastChange.score) {
|
||||
const scoreCards = Array.from(`${score}`.padStart(3, " "));
|
||||
return (
|
||||
<div className="scoreboard">
|
||||
{scoreCards.map((score, index) => (
|
||||
<Card key={`${index}-${score}`} value={score} oldValue={score} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (score !== lastChange.score && now - lastChange.timestamp > 1200) {
|
||||
const scoreCards = Array.from(`${score}`.padStart(3, " "));
|
||||
const previousScoreCards = Array.from(`${lastChange.score}`.padStart(3, " "));
|
||||
console.log("scoreCards, previousScoreCards", scoreCards, previousScoreCards);
|
||||
return (
|
||||
<div className="scoreboard">
|
||||
{scoreCards.map((score, index) => (
|
||||
<Card key={`${index}-${score}`} value={score} oldValue={previousScoreCards[index]} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (score !== lastChange.score && now - lastChange.timestamp > 1200) {
|
||||
changeLastChange({ timestamp: now, score, previousScore: lastChange.score });
|
||||
}
|
||||
|
||||
const scoreCards = Array.from(`${lastChange.previousScore}`.padStart(3, " "));
|
||||
|
||||
// console.log("now - lastChange.timestamp", now - lastChange.timestamp);
|
||||
if (score !== lastChange.previousScore) {
|
||||
const newScoreCards = Array.from(`${score}`.padStart(3, " "));
|
||||
console.log("updating");
|
||||
return (
|
||||
<div className="scoreboard">
|
||||
{newScoreCards.map(
|
||||
(score, index) =>
|
||||
console.log(score, scoreCards[index]) || (
|
||||
<Card key={`${index}-${score}`} value={score} oldValue={scoreCards[index]} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="scoreboard">
|
||||
{scoreCards.map((score, index) => (
|
||||
<Card key={`${index}-${score}`} value={score} oldValue={score} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
// const scoreCards = Array.from(`${score}`.padStart(3, " "));
|
||||
// const previousScoreCards = Array.from(`${lastScore}`.padStart(3, " "));
|
||||
|
||||
// if (lastScore < score - 1) {
|
||||
// updateScore(score);
|
||||
// }
|
||||
// console.log("score, lastScore", score, lastScore);
|
||||
// return (
|
||||
// <div className="scoreboard">
|
||||
// {scoreCards.map((score, index) => (
|
||||
// <Card key={`${index}-${score}`} value={score} oldValue={previousScoreCards[index]} />
|
||||
// ))}
|
||||
// </div>
|
||||
// );
|
||||
};
|
||||
|
||||
// const ArrowButton = styled(Button)`
|
||||
// min-width: 5rem;
|
||||
// `;
|
||||
@@ -253,7 +105,6 @@ class Snake extends React.Component {
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.supportedKeys = Object.values(keys);
|
||||
window.me = this;
|
||||
this.state = { previousPoints: 0 };
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
@@ -277,40 +128,12 @@ class Snake extends React.Component {
|
||||
snake,
|
||||
died,
|
||||
fps,
|
||||
score,
|
||||
previousScore
|
||||
score
|
||||
} = this.props;
|
||||
|
||||
if (false && died) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Title large>Snake</Title>
|
||||
<Title>
|
||||
You died{" "}
|
||||
<span role="img" aria-label="skull">
|
||||
☠️
|
||||
</span>{" "}
|
||||
with {snake.length} points.
|
||||
</Title>
|
||||
<Button onClick={startSnake}>Restart</Button>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (!started && !died) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Title large>Snake</Title>
|
||||
<Grid data={grid} />
|
||||
<Button onClick={startSnake}>Start</Button>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Title large>Snake</Title>
|
||||
<Score score={score} previousScore={previousScore} />
|
||||
<Scoreboard score={score} zoom={2} />
|
||||
<Grid data={grid} died={died} />
|
||||
<Button onClick={updateFrameSnake}>Next</Button>
|
||||
<ToggleButton toggle={paused} onClick={pauseSnake}>
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import Title from "../components/Title.js";
|
||||
import List from "../components/List.js";
|
||||
import Button from "../components/Button.js";
|
||||
import Link from "../components/Link.js";
|
||||
import Spinner from "../components/Spinner.js";
|
||||
import { getKeyboards, showKeyboard } from "../redux/keyboards.js";
|
||||
|
||||
const KeyboardListItem = ({ item, showKeyboard }) => (
|
||||
<List.Item key={item.id}>
|
||||
<Link to={`/keyboards/${item.id}`}>
|
||||
{item.maker} - {item.model}
|
||||
</Link>
|
||||
</List.Item>
|
||||
);
|
||||
|
||||
const NoKeyboardsFound = ({ spinner }) => {
|
||||
if (spinner) {
|
||||
return <p>Loading...</p>;
|
||||
}
|
||||
return <p>There are no keyboards</p>;
|
||||
};
|
||||
|
||||
class Keyboards extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.getKeyboards();
|
||||
}
|
||||
render() {
|
||||
const { keyboards, spinner, getKeyboards } = this.props;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Title large>Keyboard List</Title>
|
||||
<List items={keyboards} Item={KeyboardListItem} Empty={NoKeyboardsFound} itemProps={this.props} />
|
||||
<Button onClick={() => getKeyboards({ force: true })}>Refresh</Button>
|
||||
{spinner && <Spinner />}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
const mapState = ({ keyboards, ui }) => ({
|
||||
keyboards: keyboards.list,
|
||||
spinner: ui.spinner
|
||||
});
|
||||
|
||||
const mapActions = {
|
||||
getKeyboards,
|
||||
showKeyboard
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapState,
|
||||
mapActions
|
||||
)(Keyboards);
|
||||
@@ -14,6 +14,7 @@ export const keys = {
|
||||
RIGHT: "ArrowRight",
|
||||
DOWN: "ArrowDown",
|
||||
LEFT: "ArrowLeft",
|
||||
EQUAL: "=",
|
||||
PLUS: "+",
|
||||
MINUS: "-"
|
||||
};
|
||||
@@ -94,7 +95,7 @@ module.reducer(UNSUBSCRIBE_KEY_PRESSED, (state, { name }) => {
|
||||
|
||||
module.middleware(KEY_PRESS, (dispatch, { keyPressSubscribers = [], fps }, { key }) => {
|
||||
keyPressSubscribers.forEach(subscriber => dispatch({ type: subscriber, key }));
|
||||
if (key === keys.PLUS) {
|
||||
if (key === keys.PLUS || key === keys.EQUAL) {
|
||||
dispatch(setFps(fps + 1));
|
||||
}
|
||||
if (key === keys.MINUS) {
|
||||
|
||||
@@ -19,8 +19,7 @@ const initialState = {
|
||||
vY: 0,
|
||||
snake: [],
|
||||
score: 0,
|
||||
previousScore: 0,
|
||||
apple: randomPosition(DEFAULT_GRID_SIZE)
|
||||
apple: []
|
||||
};
|
||||
|
||||
/* === Snake Helper Functions ================================================================== */
|
||||
@@ -69,7 +68,10 @@ function createGrid(size) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const markCell = (grid, [x, y], mark) => {
|
||||
const markCell = (grid, [x, y] = [], mark) => {
|
||||
if (x === undefined || y === undefined) {
|
||||
return grid;
|
||||
}
|
||||
grid[y][x] = mark;
|
||||
return grid;
|
||||
};
|
||||
@@ -113,12 +115,12 @@ export const [UPDATE_STATE, updateState] = module.action("UPDATE_STATE", (newSta
|
||||
module.reducer(UPDATE_STATE, (state, { newState, fullUpdate }) => (fullUpdate ? newState : { ...state, ...newState }));
|
||||
module.reducer(RESET_SNAKE, (state, options) => {
|
||||
const grid = createGrid(state.size);
|
||||
const { apple } = initialState;
|
||||
const snake = options.started ? [[0, 0]] : [];
|
||||
const apple = options.started ? randomPosition(state.size) : initialState.apple;
|
||||
const snake = options.started ? [[0, 0]] : initialState.snake;
|
||||
|
||||
markCell(grid, apple, "a");
|
||||
snake.forEach(p => markCell(grid, p, "s"));
|
||||
return { ...state, ...initialState, grid, snake, ...options };
|
||||
return { ...state, ...initialState, grid, snake, apple, ...options };
|
||||
});
|
||||
|
||||
module.reducer(CHANGE_DIRECTION, (state, { direction }) => {
|
||||
@@ -142,11 +144,10 @@ module.reducer(CHANGE_DIRECTION, (state, { direction }) => {
|
||||
|
||||
module.reducer(UPDATE_FRAME_SNAKE, state => {
|
||||
const { snake, vX, vY, size } = state;
|
||||
let { apple, started, died, previousScore, score } = state;
|
||||
let { apple, started, died, score } = state;
|
||||
const grid = createGrid(size);
|
||||
const newSnake = [...snake];
|
||||
const nextPosition = moveSnakePart(newSnake[newSnake.length - 1], vX, vY, size);
|
||||
previousScore = score;
|
||||
if (comparePositions(nextPosition, apple)) {
|
||||
apple = randomPosition(size);
|
||||
// eslint-disable-next-line no-loop-func
|
||||
@@ -165,7 +166,7 @@ module.reducer(UPDATE_FRAME_SNAKE, state => {
|
||||
const brightness = (l, i) => 20 + Math.round(0.8 * ((i + 1) / l) * 100);
|
||||
newSnake.forEach((p, i) => markCell(grid, p, "s" + brightness(newSnake.length, i)));
|
||||
score = newSnake.length;
|
||||
return { ...state, grid, vX, vY, snake: newSnake, apple, started, died, previousScore, score };
|
||||
return { ...state, grid, vX, vY, snake: newSnake, apple, started, died, score };
|
||||
});
|
||||
|
||||
/* === Middleware =============================================================================== */
|
||||
|
||||
@@ -2,16 +2,20 @@ import { createGlobalStyle } from "styled-components";
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
body {
|
||||
position: relative;
|
||||
position: relative;
|
||||
padding: 0 0 4.25rem;
|
||||
box-sizing:border-box;
|
||||
margin: 0;
|
||||
background: ${props => props.theme.body.background};
|
||||
color: ${props => props.theme.body.color};
|
||||
font-family: sans-serif;
|
||||
font-family: Raleway;
|
||||
font-size: 1rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
h1, h2 {
|
||||
font-family: Lato;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default GlobalStyle;
|
||||
|
||||
Reference in New Issue
Block a user