<div id="root">
</div>
$background: #FFFFFF;
$black: #1b1e23;
$grey: #f4eed7;
$grey2: #9e9a89;
body,html {
height: 100%;
margin: 0;
font-family: sans-serif;
}
body {
background: $black;
display: flex;
justify-content: center;
align-items: center;
}
#root {
position: relative;
height: 330px;
width: 400px;
border: 1px solid $grey2;
background: $black;
border-radius: 5px;
}
.score {
position: absolute;
top: 0;
left: 10px;
padding-top: 6px;
display: flex;
color: $grey;
justify-content: space-between;
box-sizing: border-box;
height: 30px;
width: calc(100% - 20px);
border-bottom: 1px solid $grey2;
}
.menu {
position: absolute;
top: calc(50% + 15px);
width: 100%;
text-align: center;
font-size: 0.75em;
color: $grey2;
transform: translateY(-50%);
&.hidden {
display: none;
}
span {
color: $grey;
}
}
.part {
background: $grey;
width: 4px;
height: 4px;
border-radius: 2px;
position: absolute;
// transition: 300ms;
// transform: rotate(45deg);
}
@keyframes slide {
0% {
transform: translateX(-10px);
}
100% {
transform: translateX(0);
}
}
.food {
width: 4px;
height: 4px;
border: 1px solid $grey;
border-radius: 3px;
position: absolute;
transition: 0.05s;
}
@keyframes grow {
0% {
width: 0px;
height: 0px;
}
100% {
width: 4px;
height: 4px;
}
}
View Compiled
// Control with the arrow keys
// *NOTE*: in Codepen you must click on the preview port to
// for the window to register key presses
// Known bug: You can potentially turn the snake around too fast
// causing a collusion / reset
const start = {
active: true,
speed: 120, // ms
direction: "right",
snake: [[50, 70], [60, 70], [70, 70], [80, 70]], // Start with 4 block snake
food: [200, 70],
score: 0,
high_score: localStorage.getItem("high_score")
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = start;
}
startStop = manual => {
let active = this.state.active;
//console.log(localStorage.getItem('high_score'));
if (manual) {
this.setState({ active: !active });
}
// This is reading the previous state, before manual switched it
if (!active) {
this.interval = setInterval(() => this.updateSnake(), this.state.speed);
} else {
clearInterval(this.interval);
let high_score = this.state.high_score;
if (this.state.score > high_score) {
high_score = this.state.score;
}
localStorage.setItem("high_score", high_score);
this.setState({
active: false,
speed: 120, // ms
direction: "right",
snake: [[50, 70], [60, 70], [70, 70], [80, 70]], // Start with 4 block snake
food: [200, 70],
score: 0,
high_score: high_score
});
}
};
updateSnake() {
var direction = this.state.direction;
var currentSnake = this.state.snake;
var snakeHead = currentSnake[currentSnake.length - 1];
var newHead = [];
var target = this.state.food;
switch (direction) {
case "up":
newHead = [snakeHead[0], snakeHead[1] - 10];
break;
case "right":
newHead = [snakeHead[0] + 10, snakeHead[1]];
break;
case "down":
newHead = [snakeHead[0], snakeHead[1] + 10];
break;
case "left":
newHead = [snakeHead[0] - 10, snakeHead[1]];
break;
default:
newHead = [snakeHead[0], snakeHead[1]];
}
currentSnake.push(newHead);
currentSnake.forEach((val, i, array) => {
// As long as its not checking against itself...
if (i != array.length - 1) {
// Check if its colluding with its body
if (val.toString() == newHead.toString()) {
// Head has collided with body
// console.log('collide');
this.startStop(true);
}
}
});
// collusion detection
if (
newHead[0] > 390 ||
newHead[0] < 0 ||
newHead[1] > 320 ||
newHead[1] < 30
) {
// Enable this is you want the wall collusion rule
// this.startStop(true);
// This is teleporting the snake through the walls
let teleHead = currentSnake[currentSnake.length - 1];
if (newHead[0] > 390) {
teleHead[0] = teleHead[0] - 400;
currentSnake.shift();
}
if (newHead[0] < 0) {
teleHead[0] = teleHead[0] + 400;
currentSnake.shift();
}
if (newHead[1] > 320) {
teleHead[1] = teleHead[1] - 300;
currentSnake.shift();
}
if (newHead[1] < 30) {
teleHead[1] = teleHead[1] + 300;
currentSnake.shift();
}
} else {
// If food is eaten
if (newHead[0] == target[0] && newHead[1] == target[1]) {
let posX = Math.floor(Math.random() * (380 - 10 + 1)) + 10;
let posY = Math.floor(Math.random() * (280 - 40 + 1)) + 40;
posX = Math.ceil(posX / 10) * 10;
posY = Math.ceil(posY / 10) * 10;
this.setState(prevState => ({
snake: currentSnake,
food: [posX, posY],
score: prevState.score + 1
}));
} else {
currentSnake.shift();
if (this.state.active) {
this.setState({ snake: currentSnake });
}
}
}
}
handleKeys = event => {
let currentD = this.state.direction;
console.log(currentD);
let active = this.state.active;
// console.log(event.keyCode);
if (event.keyCode === 13) {
this.startStop(true);
}
if (event.keyCode === 65 && currentD != "right") {
this.setState({ direction: "left" });
this.swapClass();
}
if (event.keyCode === 68 && currentD != "left") {
this.setState({ direction: "right" });
this.swapClass();
}
if (event.keyCode === 87 && currentD != "down") {
this.setState({ direction: "up" });
this.swapClass();
}
if (event.keyCode === 83 && currentD != "up") {
this.setState({ direction: "down" });
this.swapClass();
}
};
componentDidMount() {
this.swapClass();
document.addEventListener("keydown", this.handleKeys, false);
if (this.state.active) {
this.startStop(false);
}
}
componentDidUpdate(prevProps, prevState) {
// When the state changes, check if we've reached a % 5 milestone
// Run speedUp once, but not again until next time (state updates each time snake moves)
let score = this.state.score;
if (score % 3 == 0 && score > 0 && score != prevState.score) {
this.speedUp();
}
document.addEventListener("keydown", this.handleKeys, false);
}
speedUp = () => {
let speed = this.state.speed;
if (speed > 50) {
speed = speed - 2;
}
clearInterval(this.interval);
this.interval = setInterval(() => this.updateSnake(), speed);
this.setState({ speed: speed });
};
// #root takes on the class of the direction, good for styling opportunities?
swapClass = () => {
var root = document.getElementById("root");
root.className = "";
root.className = this.state.direction;
};
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
var theSnake = this.state.snake;
var food = this.state.food;
return (
<React.Fragment>
<Menu active={this.state.active} />
<Score score={this.state.score} high_score={this.state.high_score} />
{theSnake.map((val, i) => (
<Part
transition={this.state.speed}
direction={this.state.direction}
top={val[1]}
left={val[0]}
/>
))}
<Food top={food[1]} left={food[0]} />
</React.Fragment>
);
}
}
class Score extends React.Component {
constructor(props) {
super(props);
}
render() {
let snake = this.props.snake;
return (
<div className="score">
<span>
Score: <strong>{this.props.score}</strong>
</span>
<span>
High Score: <strong>{this.props.high_score}</strong>
</span>
</div>
);
}
}
class Part extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
var classes = "part " + this.props.direction;
return (
<article
style={{
transition: this.props.transition + 50 + "ms",
top: this.props.top + "px",
left: this.props.left + "px"
}}
className={classes}
/>
);
}
}
class Food extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div
style={{ top: this.props.top + "px", left: this.props.left + "px" }}
className="food"
/>
);
}
}
class Menu extends React.Component {
constructor(props) {
super(props);
this.state = {
// message: 'Press <span>Enter</span> to start'
};
}
render() {
var menu_list = this.props.active ? "menu hidden" : "menu";
return (
<div className={menu_list}>
Press <span>enter</span> to start<br />
<span>w a s d</span> keys to control
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
View Compiled
This Pen doesn't use any external CSS resources.