DEV Community

Cover image for The "Fake Precision" Trap in Game Tooling: Solving a Multi-Role Knapsack Problem for a Black-Box Game
Chen Tao
Chen Tao

Posted on

The "Fake Precision" Trap in Game Tooling: Solving a Multi-Role Knapsack Problem for a Black-Box Game

If you have ever played a fast-growing tactical strategy game, you've likely encountered the "Fake Precision" Epidemic.

You search for an optimal team builder or army planner, and you find a dozen SEO-optimized wikis claiming:

"Our proprietary calculator rates this loadout at **9,420 Combat Power* (DPS: 312.4)!"*

It looks authoritative. It feels scientific. And it is almost certainly a complete fabrication.

Recently, while building tools for Command An Army (a tactical strategy title rapidly scaling on Roblox with millions of visits), I audited every existing community builder. What I found was startling:

  1. The Fabricators: Sites inventing arbitrary damage multipliers out of thin air to output pseudo-DPS metrics, despite the game client exposing zero health or damage floats.
  2. The Calculators-in-Disguise: Tools that claim to "build" an army, but in reality are just an HTML <input> box that sums star costs and tells you whether 4 + 3 + 3 = 10.

This frustrates players and violates a core tenet of software engineering: never present an invented heuristic as measured truth.

Here is the story of how we approached this as a discrete optimization challenge—designing an honest, deterministic Multi-Role Bounded Knapsack Solver that runs entirely client-side in under 2 milliseconds without inventing a single fake statistic.


1. The Real Domain Constraints

In Command An Army, players deploy a squad into automated, physics-driven tactical battles. The loadout mechanism enforces three rigid mathematical boundaries:

  1. Strict Cardinality Constraint: You have exactly 4 deployment slots ($k \le 4$).
  2. Bounded Budget (The Star Limit): Your squad's combined star cost cannot exceed your player Star Limit ($W \le \text{limit}$). In the game's progression curve, your star cap scales dynamically (roughly $+1$ star every 5 levels, capping at 15 stars at level 50).
  3. Multiset Selection: Players can equip duplicate units (e.g., running two Archer battalions or two Shieldmen frontlines).

The player's question is deceptively simple:

"I have 11 stars unlocked, and I own these 14 troops. What is the objectively best 4-unit squad I can field right now?"


2. Why Classic Knapsack Fails Here

At first glance, this looks like a classic 0/1 Knapsack Problem or an Exact-$k$ Choice Knapsack:

$$\max \sum v_i x_i \quad \text{subject to} \quad \sum w_i x_i \le W, \quad \sum x_i \le 4$$

In standard operations research, each item has an intrinsic value $v_i$ (e.g., profit, damage, health).

The catch? In a black-box video game without public damage equations, $v_i$ does not exist.

If you run four high-tier Lancers because they have high "stats", an opponent with a single cheap Spear Militia will dismount and slaughter your entire formation due to the underlying combat rock-paper-scissors engine. Raw single-unit value is a mirage; team synergy is topological.

An army fails when it has an unaddressed functional blind spot. An army succeeds when its roles orthogonally complement one another.


3. Replacing Fake DPS with Role Coverage Vectors

Instead of hallucinating numeric damage values, we modeled squad utility through Functional Orthogonality.

Step 1: Functional Bucketing

Every unit belongs to a designated combat archetype, but battlefield utility collapses into five primary tactical roles:

Tactical Bucket Battlefield Role Counter Dynamic
Frontline High-mass damage absorption Absorbs incoming arrow volleys
Ranged Sustained stand-off projectile DPS Shreds unshielded infantry & polearms
Anti-Ranged / Flank High-velocity cavalry disruption Breaches backlines & silences archers
Anti-Cavalry Polearms / bracing spears Halts & impales cavalry charges
General Melee Adaptable skirmishers Flexible engagement

Step 2: The Multi-Objective Scoring Function

Rather than maximizing imaginary DPS, the solver maximizes Role Entropy and Budget Saturation:

Score(S) = (Distinct Roles Covered * 100) + (Priority Role Bonus * 40) + (Total Stars Spent * 2)
Enter fullscreen mode Exit fullscreen mode
  1. Role Diversity (Weight: 100): A squad covering 4 distinct roles (Frontline + Ranged + Flank + Anti-Cavalry) will fundamentally out-survive a mono-culture squad.
  2. Tactical Stance Priorities (Weight: 40): Depending on user preference (balanced, attack, defense), specific anchor roles are prioritized.
  3. Budget Efficiency (Weight: 2): All else being equal, a composition that effectively utilizes 11/11 stars is preferred over one that leaves 3 stars sitting idle on the table.

This completely eliminates "fake numbers". The ranking criteria is mathematically transparent: Coverage first, Budget utilization second.


4. The Engineering Implementation

Because the cardinality is bounded at $k=4$, the combinatorics allow us to avoid heavy dynamic programming tables or external solvers like GLPK/Wasm.

The number of multiset combinations with replacement of $n$ available troops taken $k$ at a time is given by:

$$\left(!!{n \choose k}!!\right) = \binom{n + k - 1}{k}$$

Even if a player has unlocked 20 units:

$$\binom{20 + 4 - 1}{4} = \binom{23}{4} = 8,855 \text{ combinations}$$

Evaluating 8,855 combinations in modern V8 takes less than 1.5ms. This allowed us to execute the entire search synchronously on the main thread during input dispatch with zero WebWorker overhead.

Here is the core logic extracted from our production engine (solver.mjs):

export const MAX_ENTRIES = 4;

export const BUCKET_OF_ROLE = {
  Defense: 'Frontline',
  'Special melee': 'Frontline',
  'Melee all-rounder': 'Melee',
  Melee: 'Melee',
  Ranged: 'Ranged',
  Cavalry: 'Anti-ranged',
  Polearm: 'Anti-cavalry',
};

/** Multiset combinations with replacement (k <= 4) */
export function combinations(pool, maxPick) {
  const out = [];
  const current = [];
  const walk = (start) => {
    if (current.length > 0) out.push([...current]);
    if (current.length === maxPick) return;
    for (let i = start; i < pool.length; i += 1) {
      current.push(pool[i]);
      walk(i);
      current.pop();
    }
  };
  walk(0);
  return out;
}

export function solveLoadouts(pool, starLimit, preference = 'balanced', limit = 3) {
  if (!pool || pool.length === 0) {
    return { solutions: [], reason: 'Select at least one troop you actually own.' };
  }

  // Pre-filter: discard anything that exceeds total budget on its own
  const affordable = pool.filter((unit) => unit.starCost <= starLimit);
  if (affordable.length === 0) {
    return {
      solutions: [],
      reason: `No owned units cost <= ${starLimit} stars.`,
    };
  }

  const wantBuckets =
    preference === 'attack'
      ? ['Anti-ranged', 'Ranged', 'Melee', 'Frontline']
      : preference === 'defense'
        ? ['Frontline', 'Anti-cavalry', 'Ranged', 'Anti-ranged']
        : ['Frontline', 'Ranged', 'Anti-ranged', 'Anti-cavalry'];

  const scored = [];

  for (const picks of combinations(affordable, MAX_ENTRIES)) {
    const totalCost = picks.reduce((sum, u) => sum + u.starCost, 0);
    if (totalCost > starLimit) continue; // Budget constraint violated

    const coverageSet = new Set(picks.map((u) => BUCKET_OF_ROLE[u.role] || 'Melee'));

    // Calculate strategic alignment
    const priorityHits = wantBuckets.filter(
      (bucket, idx) => coverageSet.has(bucket) && idx < 2
    ).length;

    // Deterministic fitness function
    const score = (coverageSet.size * 100) + (priorityHits * 40) + (totalCost * 2);

    scored.push({
      picks,
      totalCost,
      remaining: starLimit - totalCost,
      coverage: [...coverageSet],
      score,
    });
  }

  if (scored.length === 0) {
    return { solutions: [], reason: 'No legal combination fits within that star limit.' };
  }

  // Sort: Highest fitness -> Maximum budget saturation -> Minimum slots
  scored.sort((a, b) => {
    if (b.score !== a.score) return b.score - a.score;
    if (b.totalCost !== a.totalCost) return b.totalCost - a.totalCost;
    return a.picks.length - b.picks.length;
  });

  return { solutions: scored.slice(0, limit) };
}
Enter fullscreen mode Exit fullscreen mode

5. What Happens in Practice? (Real-World Case Study)

When we deployed this into the live Command An Army Star Limit Solver, the difference in user experience was night and day compared to generic gaming wikis:

  1. Explainable Recommendations: When the solver suggests [Imperial Wall, Fire Archer, Lancer, Spear Vanguard], it doesn't give a nebulous number. It highlights the exact roles fulfilled:
    • Anchor: Blocks arrows and shields the backline.
    • Ranged: Sustained pressure against heavy armor.
    • Flanker: Rushes enemy snipers.
    • Anti-Cavalry: Defends against unexpected cavalry charges.
  2. Honest Failure Modes: If a user enters Star Limit = 4 and only checks 5-star legendary units, traditional sites silently glitch or drop entries. Our solver returns an explicit constraint rejection: explaining precisely why no legal state exists.
  3. Zero Layout Shift & Micro-latency: Because the entire solver lives in an isolated pure module (solver.mjs), it is executed both during Node.js unit tests (node test-solver.mjs) and on the client via React with zero hydration mismatches.

You can inspect the live solver interface and verified troop datasets directly on our companion platform:
👉 Live Tool: Command An Army Star Limit Army Builder


6. Lessons for Engineering Data-Driven Web Tools

Building niche web utilities or gaming companions is often dismissed as trivial content marketing. But when you treat it with genuine algorithmic rigor, you uncover fascinating design lessons:

  • Resist the urge to fake metrics: Users can smell artificial complexity. If your domain lacks clean quantitative metrics, optimize for structural/topological qualities (diversity, constraints, coverage) rather than synthesizing fake floating-point numbers.
  • Isolate pure computational logic: Keeping the solver as a dependency-free vanilla JS function allowed us to run 100+ automated edge-case test suites without spinning up mock DOMs or browser runtimes.
  • Bound your problem space early: Knowing our cardinality was strictly $k \le 4$ meant an exact recursive multiset enumeration was infinitely faster, simpler, and more maintainable than importing heavy linear programming packages.

Have you ever had to build a decision engine or recommender system for a completely black-box system? How did you handle the lack of ground-truth numbers? Let's discuss in the comments!

Top comments (0)