(Cool) Snake with React Hooks
One time my kid sat near me on a late hour, refusing to sleep.
“Dad, what are you doing”, he asked, looking at the black IDE with words in a foreign language.
“I’m writing a game”. I replied hiding the pard about #notSoFancyAppChallenge.
He sat there for thirty minutes until the game was ready, and then gave me one of the biggest compliments I ever received: “Dad, that was cool”.
Snake
The different cell states:
const status = {
empty:0,
head: 1,
body:2,
star:3
}
Initialise method, used to start or reset:
const init = () => {
const head = [11,10];
const tail = [10,10];
let board = Array.from({length:20},()=> {
return Array.from({length:20},() => ({status:status.empty}))
});
board[head[0]][head[1]] = {status: status.head};
board[tail[0]][tail[1]] = { status: status.body, from: [...head] };
return {
board: board,
head : [...head],
tail : [...tail]
}
}
For the snake I created three react components: Board, Row & Cell:
const Line = (props) =>{
return props.line.map((cell,j) => <Cell key={`c${j}`} cell={cell}/>)
}
const Cell = (props) =>{
return <div className={`cell${props.cell.status}`}></div>
}
const Board = () => {
....
return (<>{
game.board.map((line,i) => (<Line key={`l${i}`} line={line} />))
}<p>{staresSteps}</p>
<input type="button" onClick={restart} value="Restart" />
</>
)
React game variables:
let [game,setGame] = useState(init()); let [timeInterval,setTimeInterval] = useState(800); let [direction,setDirection] = useState([1,0]); let [staresSteps,setStaresSteps] = useState(0); let [clearTick,setClearTick] = useState(0);
Change direction listener:
useEffect(() => {
const keyPress = k=>{
switch(k.key){
case 'ArrowRight': setDirection([0,1]); break;
case 'ArrowLeft': setDirection([0,-1]); break;
case 'ArrowDown': setDirection([1,0]); break;
case 'ArrowUp': setDirection([-1,0]); break;
}
};
window.addEventListener('keydown', keyPress);
return () => {
window.removeEventListener('keydown', keyPress);
};
}, []);
Restart game onClick:
const restart = () => {
clearTimeout(clearTick);
setStaresSteps(0);
setTimeInterval(800);
setGame(init());
}
Calculation & help methods:
const isGameOver = () => {
return newHead[0] == -1 || newHead[0] == game.board.length || newHead[1] == -1 || newHead[1] == game.board.length || game.board[newHead[0]][newHead[1]].status == status.body
}
const stepOnStar = () => {
return game.board[newHead[0]][newHead[1]].status == status.star;
}
const setCell = (loc,val) => {
game.board[loc[0]][loc[1]] = val;
}
const getEmptyCells = () => {
let emptyCells = [];
game.board.map((line,i)=>{
line.map((cell,j) => {
if(cell.status === 0){
emptyCells.push([i,j]);
}
})
});
return emptyCells;
}
The movement (tick) executer:
useEffect(()=>{
setClearTick = setTimeout(() => {
clearTimeout(clearTick);
if(timeInterval > 100){
setTimeInterval(timeInterval -1);
}
},timeInterval);
return ()=> {
clearTimeout(clearTick);
}
},[game])
Calculate the new location:
const newHead = [game.head[0] + direction[0],game.head[1] + direction[1]];
const newTail = game.board[game.tail[0]][game.tail[1]].from;
setCell(newHead,{status:status.head});
setCell(game.head,{status: status.body, from:newHead});
game.head = newHead;
Move the tail, if he had not eaten stars:
if(staresSteps == 0){
setCell(game.tail,{status: status.empty, from: null});
game.tail = newTail;
}
Check for special steps:
if(isGameOver()){
clearTimeout(clearTick);
return;
}
if(stepOnStar()){
setStaresSteps(staresSteps + 3);
}else{
setStaresSteps(staresSteps===0?0:staresSteps-1);
}
Last, add new stars on the screen:
const emptyCells = getEmptyCells();
const chanceForStar = emptyCells.length/(game.board.length*game.board.length)/10;
if(Math.random()>0.9 - chanceForStar){
const starLocation = emptyCells[Math.floor(Math.random()*emptyCells.length)];
setCell(starLocation,{status: status.star});
}
All the logic above wrapped in a useEffect, listening to the tick passes:
useEffect(()=>{
...
...
...
setGame({...game});
},[timeInterval]);
Last, the CSS:
<style>
#root {
display: grid;
grid-template-columns: repeat(20, 20px [col-start]);}
#root div {
width: 20px;
height: 20px;
display: inline-block;
border: solid 1px gray;}
.cell0 {background-color: white;}
.cell1 {background-color: black;}
.cell2 {background-color: gray;;}
.cell3 {background-color: red;}
</style>
Link to the full code file on git.
Conclusion
My son told me that I’m cool 🙂