TypeScript Adventure: Conquering The Final Boss

TypeScript Adventure: Conquering The Final Boss

Building a Terminal Snake Game Step by Step

Welcome, brave TypeScript adventurers, to the ultimate challenge of our epic journey! 🐉 We've battled through functions, vanquished classes, and mastered advanced types.

Now, it's time to face the Final Boss: creating a Terminal Snake Game using everything we've learned. Let's build this game step by step, testing as we go!

Setting Up Our Battleground

First, let's prepare our project:

mkdir typescript-snake
cd typescript-snake
npm init -y
npm install typescript @types/node --save-dev
npx tsc --init

Now, update your tsconfig.json:

{
  "compilerOptions": {
    "target": "es2017",
    "module": "CommonJS",
    "outDir": "dist",
    "strict": true
  }
}

Step 1: Defining Our Game Types

Let's start by defining the core types for our game. Create a file src/types.ts:

export interface Position {
  x: number;
  y: number;
}

export interface SnakeSegment extends Position {
  next?: SnakeSegment;
}

export interface Food extends Position {}

export type Direction = 'up' | 'down' | 'left' | 'right';

export interface GameState {
  snake: SnakeSegment;
  food: Food;
  direction: Direction;
  gridSize: number;
}

These types define the structure of our game elements. The SnakeSegment is a linked list, allowing us to easily add segments as the snake grows.

Step 2: Creating the Initial Game State

Now, let's create our initial game state. Add this to src/game.ts:

import { GameState, Position, Direction } from './types';

export function createInitialGameState(gridSize: number): GameState {
  return {
    snake: { x: Math.floor(gridSize / 2), y: Math.floor(gridSize / 2) },
    food: generateFood(gridSize),
    direction: 'right',
    gridSize
  };
}

function generateFood(gridSize: number): Position {
  return {
    x: Math.floor(Math.random() * gridSize),
    y: Math.floor(Math.random() * gridSize)
  };
}

This function creates our initial game state with the snake in the middle and food at a random position.

Let's test it! Create src/index.ts:

import { createInitialGameState } from './game';

console.log(JSON.stringify(createInitialGameState(20), null, 2));

Now, compile and run:

npx tsc && node dist/index.js

You should see the initial game state logged to the console.

Step 3: Moving the Snake

Let's implement snake movement. Add these functions to src/game.ts:

export function updateGameState(state: GameState): GameState {
 const newHead = moveSnake(state.snake, state.direction);
  if (hasCollidedWithFood(newHead, state.food)) {
    return {
      ...state,
      snake: { ...newHead, next: state.snake },
      food: generateFood(state.gridSize)
    };
  }
  if (hasCollidedWithWall(newHead, state.gridSize) || hasCollidedWithSelf(newHead, state.snake)) {
    throw new Error('Game Over');
  }
  return {
    ...state,
    snake: { ...newHead, next: state.snake.next }
  };
}

function moveSnake(head: SnakeSegment, direction: Direction): Position {
  switch (direction) {
    case 'up': return { ...head, y: head.y - 1 };
    case 'down': return { ...head, y: head.y + 1 };
    case 'left': return { ...head, x: head.x - 1 };
    case 'right': return { ...head, x: head.x + 1 };
  }
}

function hasCollidedWithFood(head: Position, food: Position): boolean {
  return head.x === food.x && head.y === food.y;
}

function hasCollidedWithWall(head: Position, gridSize: number): boolean {
  return head.x < 0 || head.x >= gridSize || head.y < 0 || head.y >= gridSize;
}

function hasCollidedWithSelf(head: Position, snake: SnakeSegment): boolean {
  let current = snake.next;
  while (current) {
    if (head.x === current.x && head.y === current.y) return true;
    current = current.next;
  }
  return false;
}

These functions handle moving the snake, checking for collisions, and updating the game state.

Let's test it by updating src/index.ts:

import { createInitialGameState, updateGameState } from './game';

let state = createInitialGameState(20);

console.log('Initial state:', JSON.stringify(state, null, 2));

for (let i = 0; i < 5; i++) {
  state = updateGameState(state);
  console.log(`State after move ${i + 1}:`, JSON.stringify(state, null, 2));
}

Compile and run again to see the snake move!

Step 4: Rendering the Game

Now, let's visualize our game. Add this to src/render.ts:

import { GameState, SnakeSegment } from './types';

export function renderGame(state: GameState): string {
  const grid = Array(state.gridSize).fill(null).map(() => 
    Array(state.gridSize).fill(' ')
  );
  let currentSegment: SnakeSegment | undefined = state.snake;
  while (currentSegment) {
    grid[currentSegment.y][currentSegment.x] = '█';
    currentSegment = currentSegment.next;
  }
  grid[state.food.y][state.food.x] = '●';
  return grid.map(row => row.join('')).join('\n');
}

This function creates a string representation of our game state.

Let's test it by updating src/index.ts:

import { createInitialGameState, updateGameState } from './game';
import { renderGame } from './render';

let state = createInitialGameState(20);

console.log('Initial state:');
console.log(renderGame(state));

for (let i = 0; i < 5; i++) {
  state = updateGameState(state);
  console.log(`State after move ${i + 1}:`);
  console.log(renderGame(state));
}

Compile and run to see your snake move across the grid!

Step 5: Adding User Input

Finally, let's make our game interactive. Create src/input.ts:

import * as readline from 'readline';
import { Direction } from './types';

export function setupInput(onDirectionChange: (direction: Direction) => void): void {
  readline.emitKeypressEvents(process.stdin);
  process.stdin.setRawMode(true);
  process.stdin.on('keypress', (str, key) => {
    if (key.ctrl && key.name === 'c') {
      process.exit();
    } else if (['up', 'down', 'left', 'right'].includes(key.name)) {
      onDirectionChange(key.name as Direction);
    }
  });
}

Now, let's put it all together in src/index.ts:

import { createInitialGameState, updateGameState } from './game';
import { renderGame } from './render';
import { setupInput } from './input';
import { GameState } from './types';

let state: GameState = createInitialGameState(20);

function gameLoop() {
  console.clear();
  console.log(renderGame(state));
  try {
    state = updateGameState(state);
    setTimeout(gameLoop, 200);
  } catch (e) {
    console.log('Game Over!');
    process.exit();
  }
}

setupInput((direction) => {
  state.direction = direction;
});

console.log('Use arrow keys to control the snake. Press Ctrl+C to exit.');

gameLoop();

The Final Battle

Now, let's face our Final Boss! Compile and run the game:

npx tsc && node dist/index.js

Use the arrow keys to control your snake. Watch it grow as it eats the food (●) and try not to collide with the walls or yourself!

Congratulations, TypeScript Adventurer!

You've done it! You've conquered the Final Boss and created a fully functional Snake game using TypeScript. This project showcases your mastery of:

  • TypeScript's type system

  • Modular code structure

  • Game loop implementation

  • User input handling

  • State management

As you guide your snake through the terminal battlefield, remember how far you've come in your TypeScript journey. From basic types to advanced features, you've grown into a true TypeScript Adventurer.

What's Next? New Adventures Await!

But wait, there's more! Your TypeScript skills have prepared you for even greater adventures. On the horizon, we see the distant lands of AWS, where new challenges await:

  • Serverless Framework: Use your TypeScript prowess to create scalable, serverless applications and infrastructure as code all in one project with MonoRepos.

  • Terraform CDK: Craft infrastructure as code, defining and provisioning AWS resources with the power of TypeScript.

These new frontiers will test your skills in new ways, combining your TypeScript knowledge with cloud technologies to build even more powerful and scalable applications.

Are you ready for the next chapter of your coding adventure? The world of cloud computing awaits, and with your TypeScript skills, you're more than prepared to take it on!

Until then, keep coding, keep learning, and may your types always be strong and safe! 🚀🐍

Thank You For Joining Me on This Adventure!