Behind "Demon Castle 24-Hour Guard Shift" — Shift Scheduling (NSP) and Logical Linearization
01Shift Scheduling (NSP) & Decision Variables
The Demon Castle Guard Roster must schedule guards for 5 weekdays across 3 shifts per day: Day (D), Evening (E), and Night (N). Mathematically, this is an instance of the Nurse Scheduling Problem (NSP), a classic combinatorial optimization challenge.
We define binary decision variable $x_{m,d,s} \in \{0, 1\}$ to indicate whether monster $m$ is assigned to day $d$ and shift $s$:
Each guard can work at most one shift per day (no overlapping shifts):
Every slot must meet its target headcount $R_{d,s}$:
02Consecutive Shift Limits & Sliding Window Formulation
To prevent guard exhaustion, maximum consecutive working days $L_m$ are enforced. Instead of tracking state variables or using non-linear branching, we formulate this using a Sliding Window over consecutive days:
03Partner Synergy Linearization (McCormick Envelopes)
When two guards work the same shift, they may produce synergy bonuses (e.g. Goblin + Skeleton) or clash penalties. The interaction term is inherently non-linear ($x_A \cdot x_B$). In MILP, we introduce an auxiliary variable $\text{pair}_{A,B,d,s} \in [0, 1]$ and linearize it using McCormick envelopes:
04Implementation with WaSHI
Here is how to express these constraints using @ohtaman/washi in TypeScript:
import { Model, expr, type Var } from "@ohtaman/washi";
const model = new Model("DemonCastleShift");
// 1. Binary shift assignment variables: x[guardId][day][shift]
const x: Record<string, Record<string, Record<string, Var>>> = {};
for (const m of guards) {
x[m.id] = model.binary(days, shifts, { prefix: `x_${m.id}` });
}
// 2. Headcount requirement per shift
for (const d of days) {
for (const s of shifts) {
model.constraint(
expr`sum(${guards}, ${(m: { id: string }) => x[m.id][d][s]}) == ${demand[d][s]}`
);
}
}
// 3. At most one shift per day
for (const m of guards) {
for (const d of days) {
model.constraint(expr`sum(${x[m.id][d]}) <= 1`);
}
}
const status = await model.solve();
console.log("Status:", status);