Defining the types

We’ll build the game in three moves: define some types to give the game’s resources structure, create a Pinia store to manage state and state mutations, then create Vue components to collect input and represent our game state to the player. This part is the first move.

Since type systems are syntactic methods of enforcing software abstractions — and writing software involves reasoning about different abstractions and their relationships — let’s start our game by defining some types.

In the src folder of your project, create a new file called types.ts.

The first type we’ll create represents a resource cost:

export type Cost = {
  [key: string]: number;
} & {
  energy?: number;
  capacitors?: number;
  circuits?: number;
};
What is [key: string]: number } & {?

This is an index signature. Generally, index signatures are used when you don’t know the structure of a type ahead of time. We’re using it here as part of an intersection type so that we can have a type of optional (but known) properties.

Technically, Cost is an intersection type comprised of an index signature ([key: string]: number) and an object type with optional properties ({ energy?: number; capacitors?: number; circuits?: number; }).

const someCost: Cost = {
    energy: 10,
    circuits: 5,
    // note that the `capacitors` property is omitted
};

console.log(someCost.energy);       // Output: 10
console.log(someCost.capacitors);   // Output: undefined
console.log(someCost.circuits);     // Output: 5

Something in this game can cost any combination of the following:

  • zero or more energy
  • zero or more capacitors
  • zero or more circuits

Next, let’s create a type that represents the base costs of things that we can purchase:

export type BaseCosts = {
  [key: string]: Cost;
} & {
  energy?: Cost;
  capacitors: Cost;
  circuits: Cost;
};
Why is energy optional?

Energy is the basic resource of the game, so I don’t want it to cost anything. Since a cost is some combination of energy, capacitors, and circuits, the resulting base cost for what is essentially a free resource would be:

energy: {
  energy: 0,
  capacitors: 0,
  circuits: 0
} as Cost,

Making it optional here with the ? operator will allow us to define base costs for capacitors and circuits and elide a definition of energy costs.

Use this technique for other resources you add to the game that don’t cost anything to produce (e.g., maybe you have a research tree that just costs time instead of resources).

Why do we need to define base costs?

The base costs are used along with a few other variables to calculate the costs of each purchase. The formula — sometimes called a “growth formula” or “cost function” — goes like this:

next cost = [base cost] * [cost multiplier]^[# owned]

Here, “cost multiplier” is synonymous with “rate of growth”. For our game, we’re going to use a cost multiplier of 1.35. This is a completely arbitrary number; tweak it while you play your prototype until you settle on a value that feels nice, just make sure it’s always above 1.00. The multiplier is the rate of growth, so a larger multiplier is going to produce a more drastic exponential increase than a smaller one.

To illustrate how this works, let’s say we set the base cost for circuits to 25 energy and 10 capacitors. Using our formula, the first circuit will cost 25 energy and 10 capacitors:

next energy cost    = 25 * 1.35^0 = 25
next capacitor cost = 10 * 1.35^0 = 10

After we purchase our first capacitor, the cost of the next capacitor will be:

next energy cost    = 25 * 1.35^1 = 33
next capacitor cost = 10 * 1.35^1 = 13

The last type we’ll define has to do with our game’s state:

export type GameState = {
  [key: string]: number;
} & {
  energy: number;
  capacitors: number;
  circuits: number;
  costMultiplier: number;
  baseCosts: {
    capacitors: Cost;
    circuits: Cost;
  };
};

These types give us an expandable structure from which to build an incremental game. In the next part, we’ll use them to implement the core features of our game with a Pinia store.

Checkpoint

Before moving on, you should be able to confidently:

  • Model the game’s resources and state with TypeScript types

    types.ts defines Cost, BaseCosts, and GameState with no type errors

  • Understand the growth formula that drives purchase costs

    you can explain why the next cost rises as you own more of a resource