tag_utils.js

import { ENCUMBRANCE, VALUE_MULTIPLIER } from "./constants.js";
import { member, toSet } from "./set_utils.js";

/**
 * @module tag_utils
 * @description A set of utilities for parsing and testing tags.
 */

const BLANK_STRING_REGEX = /^\s*$/;
const STRING_SPLITTER = /\s+|([\(\)])/g;
const cache = new Map();

/**
 * Given a tag expression and a set of tags, evaluate whether the tags meet
 * the expression or not.
 *
 * @example
 * matcher('luxury | clothing', ['clothing'])
 * => true
 * matcher('luxury | clothing', ['alcohol'])
 * => false
 * matcher('(food | ammo) luxury', ['ammo'])
 * => false
 * matcher('(food | ammo) luxury', ['luxury', 'food'])
 * => true
 *
 * @param {String} expression
 * @param {Set} tags a set of tag strings to match against
 * @returns boolean true if the tags match the expression, false otherwise
 */
export function matcher(expression, tags) {
  if (typeof expression !== "string" || expression === "") {
    return false;
  }
  if (expression === "*") {
    return true;
  }
  let node = cache.get(expression);
  if (!node) {
    node = parser(expression);
    cache.set(expression, node);
  }
  return node.evaluate(Array.from(tags));
}

export function parser(string) {
  if (typeof string === "undefined" || BLANK_STRING_REGEX.test(string)) {
    return new True();
  }
  if (string instanceof Array) {
    return _parse(string);
  }
  let tokens = string.split(STRING_SPLITTER).filter((el) => el);
  return _parse(tokens);
}

function addOnMatch(tags, itemTags, value) {
  return matcher(tags, itemTags) ? value : 0;
}

// TODO: This now can just take tags
export function calculateValue(item) {
  let v = 1;
  v += addOnMatch("melee", item.tags, 2);
  v += addOnMatch("firearm", item.tags, 8);
  v += addOnMatch("preserved food", item.tags, 2);
  v += addOnMatch("rifle", item.tags, 3);

  v += addOnMatch("clothing", item.tags, v);
  v += addOnMatch("tiny jewelry", item.tags, 4);
  v += addOnMatch("hand jewelry", item.tags, 9);

  v += addOnMatch("historical", item.tags, 2);
  v += addOnMatch("luxury", item.tags, 3);
  v += addOnMatch("drug", item.tags, 7);
  v += addOnMatch("useful tiny", item.tags, 2);
  v += addOnMatch("useful hand", item.tags, 5);
  v += addOnMatch("useful small", item.tags, 8);
  v += addOnMatch("useful medium", item.tags, 10);
  v += addOnMatch("useful large", item.tags, 18);
  v += addOnMatch("scifi", item.tags, v * 2); // triple

  // these are now copied over to freq
  let freq = member(new Set(item.tags), Object.keys(VALUE_MULTIPLIER));
  v *= VALUE_MULTIPLIER[freq];

  return v;
}

export function calculateEncumbrance(tags) {
  let enc = member(tags, Object.keys(ENCUMBRANCE));
  if (!enc) {
    return 0;
  }
  let value = ENCUMBRANCE[enc];
  [, tags] = toSet(tags);
  if (tags.has("heavy")) {
    value *= 2;
  }
  return value;
}

function _parse(array) {
  if (array.length === 0) {
    return new True();
  }
  let someOf = new SomeOf();
  let allOf = new AllOf();
  for (let i = 0; i < array.length; ++i) {
    let element = array[i];
    if (element === "(") {
      let j = seek(array, i);
      let subarray = array.slice(i + 1, j);
      allOf.add(_parse(subarray));
      i = j;
    } else if (element === "|") {
      someOf.add(allOf);
      allOf = new AllOf();
    } else {
      if (element.substring(0, 1) === "-") {
        allOf.add(new Not(element.substring(1)));
      } else if (element !== "&") {
        allOf.add(new Literal(element));
      }
    }
  }
  someOf.add(allOf);
  return someOf.expressions.length === 1 ? someOf.expressions[0] : someOf;
}

function seek(array, i) {
  let indent = 0;
  for (let j = i; j < array.length; j++) {
    if (array[j] === "(") {
      indent++;
    } else if (array[j] === ")") {
      indent--;
    }
    if (i !== j && indent === 0) {
      return j;
    }
  }
  throw new Error("Mismatched parentheses at: " + i);
}

class SetBase {
  constructor() {
    this.expressions = [];
  }
  add(expr) {
    if (expr instanceof SetBase && expr.expressions.length === 1) {
      this.expressions.push(expr.expressions[0]);
    } else {
      this.expressions.push(expr);
    }
  }
}

class SomeOf extends SetBase {
  evaluate(variables) {
    return this.expressions.some((expr) => expr.evaluate(variables));
  }
  toString() {
    return `(${this.expressions.join(" | ")})`;
  }
}

class AllOf extends SetBase {
  evaluate(variables) {
    return this.expressions.every((expr) => expr.evaluate(variables));
  }
  toString() {
    return `(${this.expressions.join(" & ")})`;
  }
}

class Not {
  constructor(token) {
    this.token = token;
  }
  evaluate(variables) {
    return variables.indexOf(this.token) === -1;
  }
  toString() {
    return "-" + this.token;
  }
}

class Literal {
  constructor(token) {
    this.token = token;
  }
  evaluate(variables) {
    return variables.indexOf(this.token) !== -1;
  }
  toString() {
    return this.token;
  }
}

class True {
  evaluate(variables) {
    return true;
  }
  toString() {
    return "true";
  }
}