Implemented snake scoreboard and working towards a dedicated snake app

This commit is contained in:
2019-08-27 11:07:09 +02:00
parent 918e5fee45
commit 6bb4a9497c
10 changed files with 196 additions and 256 deletions

View File

@@ -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;

View File

@@ -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>
);

View 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;

View File

@@ -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}>

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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 =============================================================================== */

View File

@@ -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;