TypeScript + Lua

TypeScript + Lua

Nemo vel ad consectetur namut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod.

Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!

Overview

  1. Used fengari with TypeScript to develop a simple LuaVM class
  2. This LuaVM class encapsulates everything needed to instantiate a Lua VM and execute arbitrary code within.
  3. There’s also an API available to provide “hooks” in Lua (which triggers TypeScript behavior), as well as calling specific functions in Lua from TypeScript.

LuaVM.ts

Code

import * as fengari from 'fengari-web';
export const lua = fengari.lua;
export const lauxlib = fengari.lauxlib;
export const lualib = fengari.lualib;

export type LuaVMState = any;

export default class LuaVM {
  private state: LuaVMState;

  constructor() {
    this.state = lauxlib.luaL_newstate();
    // Load Lua libraries
    lualib.luaL_openlibs(this.state);
  }

  public execute(code: string): void {
    const resultCode = lauxlib.luaL_loadstring(this.state, fengari.to_luastring(code));
    if (resultCode) {
      throw new Error(lua.lua_tojsstring(this.state, -1));
    }
    lua.lua_call(this.state, 0, 0);
  }

  public callFunction(functionName: string, ...args: any[]): any {
    lua.lua_getglobal(this.state, fengari.to_luastring(functionName));

    for (const arg of args) {
      switch (typeof arg) {
        case 'boolean':
          lua.lua_pushboolean(this.state, arg ? 1 : 0);
          break;
        case 'number':
          lua.lua_pushnumber(this.state, arg);
          break;
        case 'string':
          lua.lua_pushstring(this.state, fengari.to_luastring(arg));
          break;
        default:
          throw new Error('Unsupported argument type');
      }
    }

    if (lua.lua_pcall(this.state, args.length, 1, 0) !== 0) {
      throw new Error(lua.lua_tojsstring(this.state, -1));
    }

    const type = lua.lua_type(this.state, -1);
    let returnValue;
    switch (type) {
      case lua.LUA_TNIL:
        returnValue = null;
        break;
      case lua.LUA_TBOOLEAN:
        returnValue = lua.lua_toboolean(this.state, -1);
        break;
      case lua.LUA_TNUMBER:
        returnValue = lua.lua_tonumber(this.state, -1);
        break;
      case lua.LUA_TSTRING:
        returnValue = lua.lua_tojsstring(this.state, -1);
        break;
      default:
        throw new Error('Unsupported return type');
    }

    lua.lua_pop(this.state, 1);  // Pop the returned value from stack
    return returnValue;
  }

  public registerHook(name: string, fn: (...args: any[]) => any): void {
    const luaFn = (luaState: any) => {
      const argCount = lua.lua_gettop(luaState);
      const args = [];

      for (let i = 1; i <= argCount; i++) {
        if (lua.lua_isnumber(luaState, i)) {
          args.push(lua.lua_tonumber(luaState, i));
        } else if (lua.lua_isstring(luaState, i)) {
          args.push(lua.lua_tojsstring(luaState, i));
        } else {
          args.push(null);
        }
      }

      const result = fn(...args);
      if (typeof result === 'number') {
        lua.lua_pushnumber(luaState, result);
        return 1; // Returning 1 result
      } else if (typeof result === 'string') {
        lua.lua_pushstring(luaState, fengari.to_luastring(result));
        return 1; // Returning 1 result
      }

      return 0; // Returning 0 results
    };

    lua.lua_pushjsfunction(this.state, luaFn);
    lua.lua_setglobal(this.state, fengari.to_luastring(name));
  }

  public getGlobal(variableName: string): any {
    lua.lua_getglobal(this.state, fengari.to_luastring(variableName));
    const type = lua.lua_type(this.state, -1);

    let returnValue;
    switch (type) {
      case lua.LUA_TNUMBER:
        returnValue = lua.lua_tonumber(this.state, -1);
        break;
      case lua.LUA_TSTRING:
        returnValue = fengari.lua_tojsstring(this.state, -1);
        break;
      default:
        throw new Error('Unsupported variable type');
    }

    lua.lua_pop(this.state, 1); // Pop the value from stack
    return returnValue;
  }

  public dispose(): void {
    lua.lua_close(this.state);
  }
}

Usage

const demo = new LuaVM();
demo.execute(`
  -- This is all valid Lua which will be executed by the VM.

 -- This will print to the browser console:
  print("Hello, world!")

  -- Anything executed persist in memory for later use:
  function do_math(a, b)
    return a + b;
  end
`)

// We can define Lua functions that trigger TS behavior
demo.registerHook("test", () => console.log('Hello from Lua!'));
demo.execute('test()'); // "Hello from Lua!" prints to console

// We can also call a Lua function from TypeScript,
// with its response already casted to the proper type.
const result = demo.callFunction('do_math', 1, 2);
result === 3; // true! the returned from do_math(1, 2)

Applications

  1. This is useful when you need to introduce a scripting language/environment into your application
  2. Runtime behavior can change after the software has shipped, since it’s all Lua-driven
  3. Players have the ability to write or import their own scripts and share with others

Demo

This demonstration features a half-baked card game. You can collect and spend Resource cards, and there are various actions/characters you can buy with those resources. Each time you play a card, a ’turn’ is taken. There is no point to this game, and it ends when you are done tinkering with it.

The fun part of this demo is that all of the cards are defined in Lua. There is some special case handling for the Resource card in particular, but everything else is driven through a dynamic Lua script. Click “Source” under each card to view its underlying Lua code.

Card Examples

When this card is played, player draws between 1 and 3 more cards.

function on_card_played()
  local random_number = math.random(1, 3)
  spend_player_resources(get_resource_cost())
  remove_from_hand()

  print("Drawing " .. random_number .. " extra cards!")
  for i = 1, random_number do
    draw_card()
  end
end

“Must be held for 3 turns before playing. When played, gain 2 resources. If discarded, lose 2 resources.”

local turns_in_hand = 3

function on_turn_start()
  turns_in_hand = turns_in_hand - 1
end

function can_play_card()
  return turns_in_hand <= 0
end

function on_card_played()
  draw_resource_card()
  draw_resource_card()
  remove_from_hand()
end

function on_card_discarded()
  spend_player_resources(2)
end

Simple check to determine if the card should be removed from play after a number of turns.

function on_turn_start()
    if hasPlayed then
      age = age - 1
      if age <= 0 then
        remove_from_playspace()
      end
    end
end

“When played, you have a 100% chance to draw 3 extra cards. Every subsequent turn, the chance reduces by half until the card is removed from play.”

local bonus_chance = 1
function on_turn_start()
    bonus_chance = bonus_chance / 2
    if bonus_chance < 0.01 then
        bonus_chance = 0
        print("The threads of fate have unwound. The Fate Weaver's power has waned.")
        remove_from_hand()
    end
end

function on_card_played()
    spend_player_resources(get_resource_cost())
    remove_from_hand()

    if math.random() <= bonus_chance then
        print("Luck favors you! Drawing 3 extra cards.")
        for i = 1, 3 do
            draw_card()
        end
    else
        print("Luck is elusive this time.")
    end
end

Related Posts

TS: Casting to Interfaces at Runtime

TS: Casting to Interfaces at Runtime

In C#/Unity, there is an excellent affordance of using GetComponent to retrieve components based on their interface.

Read More
Halton Sequence

Halton Sequence

Nemo vel ad consectetur namut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat.

Read More
Heatmap

Heatmap

Simple Heatmap class used to find areas of interest for real-time applications, like games.

Read More