(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 🙂