Setting Up the Project: Main files and Dependencies

by Hexagon, 4 minutes read deno tetris guide deno-tetris-guide

Welcome to the second part of our series on building Detris, a web-based Tetris game using Deno. In this article, we'll take you through the initial steps of setting up your project environment. We'll cover the importance of the deps.ts and main.ts files, setting the foundation for the game development that lies ahead. By the end of this article, you'll have a base for the project and be ready to dive into the exciting world of frontend development!



Setting Up the Project: Main files and Dependencies

Adding the Main Files

We'll start with the basics, setting up the HTTP server, starting the game loop, and adding routes to the server.

All files we will go through is available at the Detris GitHub repository, but adding them manually, and carefully examining the code will make it easier to customize it later.

deps.ts

In Deno, deps.ts is like a central place where you manage your project's dependencies. Think of it as the `package.json`` but for Deno. It's a neat way to keep all your imports in one place so you can easily manage them.

export { serve } from "https://deno.land/std@0.190.0/http/server.ts";
export { serveFile } from "https://deno.land/std@0.190.0/http/file_server.ts";
export { join, resolve } from "https://deno.land/std@0.190.0/path/mod.ts";

Here, we're importing three main dependencies:

  • serve: Used to create an HTTP server.
  • serveFile: Used for serving files over HTTP.
  • join, resolve: These are path manipulation utilities.

main.ts

The main.ts file is where the magic happens. This is your server's entry point, and it's where you'll put most of your backend logic.

There is some imports here that doesn't exist in your project yet, I will add these later.

The MainLoop function is where our game logic will reside. It's going to be called repeatedly to update the game state.

The serve function is used to set up an HTTP server that listens for incoming requests.

import { serve } from "./deps.ts";
import { serveFile } from "./deps.ts";
import { join, resolve } from "./deps.ts";

import * as highscore from "./highscores/highscores.ts";

import { Player } from "./server/player.ts";
import { Router } from "./server/router.ts";
import { Game } from "./game/game.ts";

// Main game loop
const games: Game[] = [];
const MainLoop = () => {

  // Wrap the full main login in a try catch
  try {
    for (const game of games) {
      // Start created games when requirements are met
      if (game.getStatus() === "created") {
        // Start if requirements are met
        if (game.checkRequirements()) {
          game.start();

          // Abandon game if not started after 120 seconds
        } else if (game.getCreateTime() < Date.now() - 120_000) {
          game.abandon();
        }
      }

      // Iterate games when playing
      if (game.getStatus() === "playing") {
        if (!game.iterate()) {
          game.over();
        }
      }

      // Update highscore if score changed
      if (game.scoreChanged()) {
        // Determine nickname
        let nickname = "";
        let score = 0;
        let level = 0;
        let lines = 0;
        if (game.listPlayers().length == 2) {
          if (game.getMode() === "battle") {
            const winner = (game.getData() as { Winner: number }).Winner;
            if (winner >= 0) {
              nickname = game.listPlayers()[winner].getNickname();
              score = (game.getData() as { Score: number[] }).Score[winner];
            }
          } else if (game.getMode() === "coop") {
            nickname = game.listPlayers()[0].getNickname() + " & " +
              game.listPlayers()[1].getNickname();
            score = (game.getData() as { Score: number }).Score;
            level = (game.getData() as { Levels: number }).Levels;
            lines = (game.getData() as { Lines: number }).Lines;
          }
        } else {
          nickname = game.listPlayers()[0].getNickname();
          score = (game.getData() as { Score: number }).Score;
          level = (game.getData() as { Levels: number }).Levels;
          lines = (game.getData() as { Lines: number }).Lines;
        }

        // Write highscore
        highscore.write(game.getMode(), {
          nickname,
          score,
          level,
          lines,
          ts: new Date(),
          tsInit: game.getCreateTime(),
        });
      }

      // Cleanup ended games
      const ended = game.getStatus() == "gameover" ||
        game.getStatus() == "abandoned";
      if (ended) {
        game.setCleanupTimer();

        // Remove current game if it was ended more than 120 seconds ago
        if (
          game.getCleanupTimer() &&
          game.getCleanupTimer() as number < Date.now() - 120_000
        ) {
          console.log("Cleaning up old game");
          games.splice(games.indexOf(game), 1);
        }
      }
    }
  } catch (e) {
    console.error("Main Loop Error: ", e);
  }

  // Recurse
  setTimeout(() => {
    MainLoop();
  }, 50);
};

// HTTP server
serve(async (req: Request) => {
  let pathname = new URL(req.url).pathname;

  // Serve using websockets
  if (req.headers.get("upgrade") == "websocket") {
    const { socket, response } = Deno.upgradeWebSocket(req);
    new Player(socket, games);
    return response;
  }

  // Serve using router
  const routerResponse = Router(req);
  if (routerResponse) return routerResponse;

  // Serve static files, or 404
  if (pathname === "/") pathname = "/index.html";
  const resp = await serveFile(req, resolve(join("./assets/", pathname)));

  if (resp.status === 200) {
    /* Append custom headers to successful static file responses */
    resp.headers.set("Cache-Control", "public, max-age=86400"); // 86400 seconds is 24 hours
  }

  return resp;
}, {
  // Port number resolution
  //
  // 1. Environment variable PUP_CLUSTER_PORT
  // 2. Environment variable DETRIS_PORT
  // 3. Static port 8080
  //
  port: parseInt(
    Deno.env.get("PUP_CLUSTER_PORT") || Deno.env.get("DETRIS_PORT") || "8080",
    10,
  ),
});

MainLoop();

We also have server/router.ts which takes care of the available http routes.

// server/router.ts

import { Application } from "../application.meta.ts";
import { read, readPlaying, readToday } from "../highscores/highscores.ts";

const routes = [
  {
    pattern: new URLPattern({ pathname: "/api/meta" }),
    handler: function (
      _req: Request,
      _match: Record<string, string>,
    ): Response | undefined {
      const meta = {
        ...Application,
        instance: Deno.env.get("PUP_CLUSTER_INSTANCE") || "0"
      };
      return new Response(JSON.stringify(meta), {
        status: 200,
        headers: {
          "content-type": "application/json; charset=utf-8",
        },
      });
    },
  },
  {
    pattern: new URLPattern({ pathname: "/api/highscores/:mode" }),
    handler: async function (
      _req: Request,
      match: Record<string, string>,
    ): Promise<Response | undefined> {
      // Read highscores
      if (match.mode) {
        const response = await read(match.mode);
        return new Response(JSON.stringify(response), {
          status: 200,
          headers: {
            "content-type": "application/json; charset=utf-8",
          },
        });
      }
    },
  },
  {
    pattern: new URLPattern({ pathname: "/api/playing/;mode" }),
    handler: async function (
      _req: Request,
      match: Record<string, string>,
    ): Promise<Response | undefined> {
      // Read highscores
      if (match.mode) {
        const response = await readPlaying(match.mode);
        return new Response(JSON.stringify(response), {
          status: 200,
          headers: {
            "content-type": "application/json; charset=utf-8",
          },
        });
      }
    },
  },
  {
    pattern: new URLPattern({ pathname: "/api/today/:mode" }),
    handler: async function (
      _req: Request,
      match: Record<string, string>,
    ): Promise<Response | undefined> {
      // Read highscores
      if (match.mode) {
        const response = await readToday(match.mode);
        return new Response(JSON.stringify(response), {
          status: 200,
          headers: {
            "content-type": "application/json; charset=utf-8",
          },
        });
      }
    },
  },
  // You can add more routes here...
];

export function Router(req: Request): Promise<Response> | undefined {
  for (const route of routes) {
    const match = route.pattern.exec(req.url);
    if (match) {
      return route.handler(req, match.pathname.groups);
    }
  }
  return undefined;
}

And that's it for this article! Up next, we'll dive into building the frontend.

Stay tuned!


Introduction to Detris: Web-Based Tetris Using Deno Building the Frontend