Getting Started

While the full game is playable in the browser, it is also possible to play via the npm package programming-game and code running on your machine.

Quick Start

If you're trying to get up and running quickly, you can clone the starter-template.

Recommended Code Structure

The browser editor requires a src/on-tick.ts file to export your onTick function.

In order to act in the world of Programming Game, you need to return a player action from the onTick function.

src/on-tick.ts
import { OnTick } from 'programming-game/types';

export const onTick: OnTick = (heartbeat) => {
  const { player } = heartbeat;
  return player.idle();
}

If you're using the npm package, we recommend creating an index.ts file that imports your onTick function. This will allow your codebase to work in both the browser and in node.

index.ts
import { connect } from 'programming-game';
import { onTick } from './src/on-tick';

connect({
  credentials: {
    // find these credentials at https://programming-game.com/dashboard
    userId: 'your-user-id',
    apiKey: 'your-api-key',
  },
  onTick,
});

Adapting to the state of the world

onTick is called with every update from the server. If you want your character to move in a particular direction, you'll need to return an action relative to its current position.

The example below will move the player to the right indefinitely.

src/on-tick.ts
import { OnTick } from 'programming-game/types';

export const onTick: OnTick = (heartbeat) => {
  const { player } = heartbeat;

  // venture deeper into the world
  return player.move({
    x: player.position.x + 1,
    y: player.position.y
  });
}

Defending Yourself

When you stray far enough from the safety of town, you'll begin to encounter hostile creatures. It's important to defend yourself against those that would do you harm.

In the simplest form, you can look for nearby monsters, and attempt to attack them.

src/on-tick.ts
import { OnTick } from 'programming-game/types';

export const onTick: OnTick = (heartbeat) => {
  const { player, units } = heartbeat;

  // fight any nearby monsters
  const monster = Object.values(units)
    .find(unit => unit.type === 'monster');

  if (monster) {
    return player.attack(monster);
  }

  // ... venture deeper into the world
}

Maintaining your health

Any damage you take will be healed over time by converting your calories to hp, but the further you venture from town, the more dangerous the world becomes.

When you're low on health, you can return to town — there's a healer there that will heal you.

src/on-tick.ts
import { OnTick } from 'programming-game/types';

export const onTick: OnTick = (heartbeat) => {
  const { player } = heartbeat;

  // if we're low on health, retreat back to town
  if (player.hp < player.stats.maxHp * 0.5) {
    return player.move({ x: 0, y: 0 });
  }

  // ... fight any nearby monsters
  // ... venture deeper into the world
}

Quests

Some NPCs offer quests that you can complete for rewards. Some items can only be obtained through quests, but some quests may offer rewards that are not worth the effort. Completing quests might also unlock new quests.

The following snippet will use acceptQuest to take on any quests that are available.

src/on-tick.ts
import { OnTick } from 'programming-game/types';

export const onTick: OnTick = (heartbeat) => {
  const { player, units, constants } = heartbeat;

  // accept any quests that are available
  const questCount = Object.keys(player.quests).length;
  if (questCount < constants.maxActiveQuests) {
    for (const unit of Object.values(units)) {
      if (unit.type === 'npc') {
        for (const quest of Object.values(unit.availableQuests)) {
          return player.acceptQuest(unit, quest.id);
        }
      }
    }
  }

  // ... return to town if we're low on health
  // ... fight any nearby monsters
  // ... venture deeper into the world
}

Completing Quests

The client will keep track of your active quests, as well as their progress. But it's up to you to determine whether they're ready to turn in.

You can check on a quest's progress by checking each of its steps. When you're ready to turn in a quest, you can return the turnInQuest action.

src/on-tick.ts
import { OnTick, ActiveQuest } from 'programming-game/types';

function isComplete(quest: ActiveQuest) {
  // check if all steps are complete
  return quest.steps.every((step) => {
    if (step.type === 'kill')
      return Object.values(step.targets).every(
        (target) => target.killed >= target.required,
      );
    if (step.type === 'goto') return step.completed;
    if (step.type === 'turn_in') return true;
    return false;
  })
}

export const onTick: OnTick = (heartbeat) => {
  const { player, units } = heartbeat;

  // turn in any completed quests
  for (const quest of Object.values(player.quests)) {
    const unit = units[quest.end_npc];
    if (unit?.type === 'npc' && isComplete(quest)) {
      return player.turnInQuest(unit, quest.id);
    }
  }

  // ... accept any quests that are available
  // ... return to town if we're low on health
  // ... fight any nearby monsters
  // ... venture deeper into the world
}

Handling Death

Dying is inevitable, and painful. While you can always respawn back at town, you lose everything you were carrying.

src/on-tick.ts
import { OnTick } from 'programming-game/types';

export const onTick: OnTick = (heartbeat) => {
  const { player } = heartbeat;

  // respawn if we're dead
  if (player.hp <= 0) {
    return player.respawn();
  }

  // ... turn in any completed quests
  // ... accept any quests that are available
  // ... return to town if we're low on health
  // ... fight any nearby monsters
  // ... venture deeper into the world
}

Putting It All Together

The following snippet puts all of the above together into a single function.

src/on-tick.ts
import { OnTick, ActiveQuest } from 'programming-game/types';

function isComplete(quest: ActiveQuest) {
  // check if all steps are complete
  return quest.steps.every((step) => {
    if (step.type === 'kill')
      return Object.values(step.targets).every(
        (target) => target.killed >= target.required,
      );
    if (step.type === 'goto') return step.completed;
    if (step.type === 'turn_in') return true;
    return false;
  })
}

export const onTick: OnTick = (heartbeat) => {
  const { player, units, constants } = heartbeat;

  // respawn if we're dead
  if (player.hp <= 0) {
    return player.respawn();
  }

  // turn in any completed quests
  for (const quest of Object.values(player.quests)) {
    const unit = units[quest.end_npc];
    if (unit?.type === 'npc' && isComplete(quest)) {
      return player.turnInQuest(unit, quest.id);
    }
  }

  // accept any quests that are available
  const questCount = Object.keys(player.quests).length;
  if (questCount < constants.maxActiveQuests) {
    for (const unit of Object.values(units)) {
      if (unit.type === 'npc') {
        for (const quest of Object.values(unit.availableQuests)) {
          return player.acceptQuest(unit, quest.id);
        }
      }
    }
  }

  // return to town if we're low on health
  if (player.hp < player.stats.maxHp * 0.5) {
    return player.move({ x: 0, y: 0 });
  }

  // fight any nearby monsters
  const monster = Object.values(units)
    .find(unit => unit.type === 'monster');

  if (monster) {
    return player.attack(monster);
  }

  // venture deeper into the world
  return player.move({
    x: player.position.x + 1,
    y: player.position.y
  });
}

What's Next?

This covers the absolute basics of the world, you'll need to learn to manage your inventory, acquire and eat food, craft items, use spells, and party with others.