Inspection Game
A ranger who patrols just 40% of sites can still stop poaching - but only if the penalty is harsh enough. Soften it and the same patrols let crime pay.
Level: Intermediate
Inspection game: does crime pay?
A ranger can only patrol m of n sites each day, yet must deter several
adaptive poachers. Getting caught costs a poacher a heavy penalty; a
successful raid earns a reward. The twist: you don't need to watch every
site. Poaching pays only when the expected reward beats the expected
penalty — when coverage m/n falls below reward / (reward + penalty).
The knob is penalty. With coverage fixed at 40%, a lenient penalty
leaves poaching profitable (payoff > 0); a harsh one deters it (payoff < 0).
import random
from tys import probe, progress
def simulate(cfg: dict):
"""Simulate repeated patrols and adaptive poachers."""
import simpy
env = simpy.Environment()
n_sites = cfg["num_sites"] # total locations that could be patrolled
patrols = cfg["patrols"] # how many sites the ranger covers each day
num_poachers = cfg["num_poachers"]
reward = cfg["reward"] # gain for an uncaught poacher
penalty = cfg["penalty"] # cost if caught red-handed
lr = cfg.get("learning_rate", 0.1) # update weight for reinforcement
eps = cfg.get("epsilon", 0.1) # exploration probability
sim_time = cfg["sim_time"]
rng = random.Random(cfg.get("seed", 123))
Each poacher tracks estimated value per site.
q_values = [[0.0 for _ in range(n_sites)] for _ in range(num_poachers)]
catch_count = 0
attempts = 0
total_payoff = 0.0
done = env.event()
One step represents a day of patrols and poaching.
def day():
nonlocal catch_count, attempts, total_payoff
for t in range(sim_time):
patrol_sites = rng.sample(range(n_sites), k=patrols)
probe("ranger_coverage", env.now, patrols / n_sites)
for p in range(num_poachers):
q = q_values[p]
if rng.random() < eps:
site = rng.randrange(n_sites)
else:
best = max(q)
best_sites = [i for i, v in enumerate(q) if v == best]
site = rng.choice(best_sites)
attempts += 1
if site in patrol_sites:
catch_count += 1
payoff = -penalty
else:
payoff = reward
simple reinforcement update
q[site] = (1 - lr) * q[site] + lr * payoff
total_payoff += payoff
catch_rate = catch_count / attempts
avg_payoff = total_payoff / attempts
probe("catch_rate", env.now, catch_rate)
probe("poacher_payoff", env.now, avg_payoff)
progress(100 * (t + 1) / sim_time)
yield env.timeout(1)
done.succeed({
"catch_rate": catch_rate,
"avg_payoff": avg_payoff,
"poaching_pays": avg_payoff > 0,
})
env.process(day())
env.run(until=done)
return done.value
def requirements():
return {
"external": ["simpy==4.1.1"],
}
# lenient.yaml — the default: the penalty (5) is only half the reward (10),
# so even with 40% coverage poaching pays (poacher_payoff stays positive).
num_sites: 5
patrols: 2
num_poachers: 3
reward: 10
penalty: 5
learning_rate: 0.2
epsilon: 0.1
sim_time: 100
seed: 123
| Metric | Value |
|---|---|
| catch_rate | 0.37 |
| avg_payoff | 4.40 |
| poaching_pays | true |
# deterrent.yaml — the contrast: identical except penalty (5 -> 50). The same
# 40% coverage now deters poaching because the expected penalty outweighs the
# reward (poacher_payoff goes negative).
num_sites: 5
patrols: 2
num_poachers: 3
reward: 10
penalty: 50
learning_rate: 0.2
epsilon: 0.1
sim_time: 100
seed: 123
| Metric | Value |
|---|---|
| catch_rate | 0.41 |
| avg_payoff | -14.60 |
| poaching_pays | false |
- When does poaching pay?
- When the expected reward beats the expected penalty: poaching is profitable while coverage (patrols/num_sites) stays below reward/(reward+penalty). At reward 10 and penalty 5 that threshold is 0.67, well above the 0.4 coverage, so poaching pays; at penalty 50 it drops to 0.17, below 0.4, so poaching is deterred.
- Why does only the penalty change between the two runs?
- Coverage (40%) and every other parameter are frozen, so the poacher_payoff line crossing from positive to negative is caused by the penalty alone - severity substituting for inspection rate.
- How do poachers choose a site?
- Each poacher keeps a Q-value per site and updates it after every raid (q = (1-lr)*q + lr*payoff), mostly picking its best-valued site but exploring at random with probability epsilon.
- What is the real-world lesson?
- Random audits, tax inspections, or transit fare checks deter cheating without checking everyone - a low inspection probability paired with a steep penalty can be enough.