Game Scripting

Entity system with scripted queries, commands, and step limits.

← Back to use cases

Full source

This is a self-contained C++ program. Copy it, compile against the mino library, and run it.

/*
 * game_scripting.cpp - entity system with scripted behaviors.
 *
 * A game engine exposes its entity system through typed handles.
 * Scripts define behaviors, queries, and commands. Step limits
 * protect the host from runaway player-authored code. Entity
 * state is queried through getters, never mutated directly by
 * the script.
 *
 * Build:
 *   make
 *   c++ -std=c++17 -Isrc -o examples/use-cases/game_scripting \
 *       examples/use-cases/game_scripting.cpp src/[a-z]*.o -lm
 */

#include "mino.h"
#include <cstdio>
#include <cstring>
#include <vector>
#include <string>

/* ── Expose ────────────────────────────────────────────────────────── */

/* Entities have a name, position, health, and tags. The C++ side
 * owns all mutation. Scripts read entity state through getters
 * and issue commands that the host validates and applies. */

struct Entity {
    std::string name;
    double x, y;
    int hp, max_hp;
    std::vector<std::string> tags;
};

static std::vector<Entity> world;

static int find_entity(const char *name, size_t len)
{
    for (size_t i = 0; i < world.size(); i++)
        if (world[i].name.size() == len &&
            strncmp(world[i].name.c_str(), name, len) == 0)
            return (int)i;
    return -1;
}

/* Return an entity as a mino map snapshot. */
static mino_val_t *entity_to_map(mino_state_t *S, const Entity &e)
{
    /* Build tags vector. */
    std::vector<mino_val_t *> tag_vals;
    for (auto &t : e.tags)
        tag_vals.push_back(mino_keyword(S, t.c_str()));
    mino_val_t *tags = mino_vector(S, tag_vals.data(), tag_vals.size());

    mino_val_t *ks[5], *vs[5];
    ks[0] = mino_keyword(S, "name"); vs[0] = mino_string(S, e.name.c_str());
    ks[1] = mino_keyword(S, "x");    vs[1] = mino_float(S, e.x);
    ks[2] = mino_keyword(S, "y");    vs[2] = mino_float(S, e.y);
    ks[3] = mino_keyword(S, "hp");   vs[3] = mino_int(S, e.hp);
    ks[4] = mino_keyword(S, "tags"); vs[4] = tags;
    return mino_map(S, ks, vs, 5);
}

/* Host function: look up entity by name, return snapshot. */
static mino_val_t *host_entity(mino_state_t *S, mino_val_t *args,
                               mino_env_t *)
{
    const char *name;
    size_t len;
    if (!mino_is_cons(args) ||
        !mino_to_string(mino_car(args), &name, &len))
        return mino_nil(S);
    int idx = find_entity(name, len);
    if (idx < 0) return mino_nil(S);
    return entity_to_map(S, world[(size_t)idx]);
}

/* Host function: list all entities as a vector of maps. */
static mino_val_t *host_entities(mino_state_t *S, mino_val_t *,
                                 mino_env_t *)
{
    std::vector<mino_ref_t *> refs;
    for (auto &e : world)
        refs.push_back(mino_ref(S, entity_to_map(S, e)));

    std::vector<mino_val_t *> items;
    for (auto *r : refs)
        items.push_back(mino_deref(r));
    mino_val_t *result = mino_vector(S, items.data(), items.size());
    for (auto *r : refs)
        mino_unref(S, r);
    return result;
}

/* Host function: move an entity by dx, dy. */
static mino_val_t *host_move(mino_state_t *S, mino_val_t *args,
                             mino_env_t *)
{
    const char *name;
    size_t len;
    if (!mino_is_cons(args) ||
        !mino_to_string(mino_car(args), &name, &len))
        return mino_nil(S);
    args = mino_cdr(args);
    long long dx = 0, dy = 0;
    if (mino_is_cons(args)) { mino_to_int(mino_car(args), &dx); args = mino_cdr(args); }
    if (mino_is_cons(args)) { mino_to_int(mino_car(args), &dy); }
    int idx = find_entity(name, len);
    if (idx < 0) return mino_nil(S);
    world[(size_t)idx].x += dx;
    world[(size_t)idx].y += dy;
    return entity_to_map(S, world[(size_t)idx]);
}

/* Host function: apply damage to an entity. */
static mino_val_t *host_damage(mino_state_t *S, mino_val_t *args,
                               mino_env_t *)
{
    const char *name;
    size_t len;
    if (!mino_is_cons(args) ||
        !mino_to_string(mino_car(args), &name, &len))
        return mino_nil(S);
    long long amount = 0;
    if (mino_is_cons(mino_cdr(args)))
        mino_to_int(mino_car(mino_cdr(args)), &amount);
    int idx = find_entity(name, len);
    if (idx < 0) return mino_nil(S);
    world[(size_t)idx].hp -= (int)amount;
    if (world[(size_t)idx].hp < 0) world[(size_t)idx].hp = 0;
    return mino_int(S, world[(size_t)idx].hp);
}

/* ── Script ────────────────────────────────────────────────────────── */

/* Scripts define queries and commands over the entity system.
 * Keywords like `:hp` and `:tags` act as data accessors. Sets act
 * as predicates for tag matching. */

static const char *script =
    ";; Find entities within a radius of a point.\n"
    "(defn nearby [x y radius]\n"
    "  (let [r2 (* radius radius)]\n"
    "    (->> (all-entities)\n"
    "         (filter (fn [e]\n"
    "                   (let [dx (- (:x e) x)\n"
    "                         dy (- (:y e) y)]\n"
    "                     (<= (+ (* dx dx) (* dy dy)) r2)))))))\n"
    "\n"
    ";; Find all entities with a given tag.\n"
    "(defn tagged [tag]\n"
    "  (->> (all-entities)\n"
    "       (filter (fn [e] (some #{tag} (:tags e))))))\n"
    "\n"
    ";; Summary: name and HP of all hostile entities.\n"
    "(defn hostile-report []\n"
    "  (->> (tagged :hostile)\n"
    "       (map (fn [e] [(:name e) (:hp e)]))\n"
    "       (into (sorted-map))))\n"
    "\n"
    ";; Run a scripted scenario.\n"
    "(println \"--- entities ---\")\n"
    "(println (mapv :name (all-entities)))\n"
    "\n"
    "(println \"\\n--- nearby (0,0) r=15 ---\")\n"
    "(println (mapv :name (nearby 0 0 15)))\n"
    "\n"
    "(println \"\\n--- hostile report ---\")\n"
    "(println (hostile-report))\n"
    "\n"
    "(move-entity \"goblin-1\" 5 0)\n"
    "(damage-entity \"goblin-1\" 30)\n"
    "\n"
    "(println \"\\n--- after combat ---\")\n"
    "(println (hostile-report))\n";

/* ── Embed ─────────────────────────────────────────────────────────── */

int main()
{
    /* Populate the game world. */
    world = {
        {"player",   0.0,   0.0, 100, 100, {"player", "friendly"}},
        {"goblin-1", 8.0,   3.0,  45,  45, {"hostile", "goblin"}},
        {"goblin-2", 25.0, 10.0,  40,  40, {"hostile", "goblin"}},
        {"merchant", 5.0,  -2.0,  80,  80, {"friendly", "npc"}},
        {"dragon",   50.0, 50.0, 500, 500, {"hostile", "boss"}},
    };

    mino_state_t *S   = mino_state_new();
    mino_env_t   *env = mino_env_new_default(S);

    /* Step limit: protect against runaway player scripts. */
    mino_set_limit(S, MINO_LIMIT_STEPS, 100000);

    /* Register entity functions. */
    mino_register_fn(S, env, "entity",        host_entity);
    mino_register_fn(S, env, "all-entities",  host_entities);
    mino_register_fn(S, env, "move-entity",   host_move);
    mino_register_fn(S, env, "damage-entity", host_damage);

    mino_val_t *result = mino_eval_string(S, script, env);
    if (!result)
        fprintf(stderr, "error: %s\n", mino_last_error(S));

    mino_env_free(S, env);
    mino_state_free(S);
}
Build and run:
c++ -std=c++17 -O2 \
  -Imino/src -Imino/src/public -Imino/src/runtime -Imino/src/gc -Imino/src/eval \
  -Imino/src/collections -Imino/src/prim -Imino/src/async -Imino/src/interop \
  -Imino/src/diag -Imino/src/vendor/imath \
  -o use-cases/game_scripting \
  use-cases/game_scripting.cpp \
  mino/src/public/*.c mino/src/runtime/*.c mino/src/gc/*.c mino/src/eval/*.c \
  mino/src/collections/*.c mino/src/prim/*.c mino/src/async/*.c mino/src/interop/*.c \
  mino/src/regex/*.c mino/src/diag/*.c mino/src/vendor/imath/*.c \
  -lm
./use-cases/game_scripting

The mino script

Scripts define queries and commands over the entity system. Keywords like :hp and :tags act as data accessors. Sets act as predicates for tag matching.

;; Find entities within a radius of a point.
(defn nearby [x y radius]
  (let [r2 (* radius radius)]
    (->> (all-entities)
         (filter (fn [e]
                   (let [dx (- (:x e) x)
                         dy (- (:y e) y)]
                     (<= (+ (* dx dx) (* dy dy)) r2)))))))

;; Find all entities with a given tag.
(defn tagged [tag]
  (->> (all-entities)
       (filter (fn [e] (some #{tag} (:tags e))))))

;; Summary: name and HP of all hostile entities.
(defn hostile-report []
  (->> (tagged :hostile)
       (map (fn [e] [(:name e) (:hp e)]))
       (into (sorted-map))))

;; Run a scripted scenario.
(println \"--- entities ---\")
(println (mapv :name (all-entities)))

(println \"
--- nearby (0,0) r=15 ---\")
(println (mapv :name (nearby 0 0 15)))

(println \"
--- hostile report ---\")
(println (hostile-report))

(move-entity \"goblin-1\" 5 0)
(damage-entity \"goblin-1\" 30)

(println \"
--- after combat ---\")
(println (hostile-report))

← All use cases