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:
- 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.
-
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 whether4 + 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:
- Strict Cardinality Constraint: You have exactly 4 deployment slots ($k \le 4$).
- 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).
- 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)
- Role Diversity (Weight: 100): A squad covering 4 distinct roles (Frontline + Ranged + Flank + Anti-Cavalry) will fundamentally out-survive a mono-culture squad.
-
Tactical Stance Priorities (Weight: 40): Depending on user preference (
balanced,attack,defense), specific anchor roles are prioritized. - 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) };
}
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:
-
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.
-
Honest Failure Modes: If a user enters
Star Limit = 4and 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. -
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)