Game-Theoretic Optimization of Attack Defense Strategies

Finding Strategic Equilibrium Between Attackers and Defenders


Introduction

Imagine you’re a cybersecurity manager deciding how to allocate your limited defense budget. Your adversary — a rational attacker — is simultaneously deciding which systems to target. Neither of you knows exactly what the other will do, yet both of you are trying to optimize your outcome. This is precisely the setting where Game Theory shines.

In this post, we’ll model the attacker-defender interaction as a two-player zero-sum game, compute the Nash Equilibrium (mixed strategy), and visualize the strategic landscape in both 2D and 3D. We’ll use nashpy, scipy, and matplotlib — all runnable in Google Colaboratory.


Problem Setup

The Players

  • Defender (Player 1): Chooses which of $m$ assets to protect (or how to allocate defense across assets).
  • Attacker (Player 2): Chooses which of $n$ assets to attack.

The Payoff Matrix

Let $A \in \mathbb{R}^{m \times n}$ be the payoff matrix from the Defender’s perspective:

$$A_{ij} = \text{Defender’s payoff when defending asset } i \text{ and attacker targets asset } j$$

Since the game is zero-sum, the attacker’s payoff is $-A_{ij}$.

Nash Equilibrium (Mixed Strategies)

A mixed strategy Nash Equilibrium is a pair $(\mathbf{x}^*, \mathbf{y}^*)$ where:

$$\mathbf{x}^* = \arg\max_{\mathbf{x}} \min_{\mathbf{y}} \mathbf{x}^\top A \mathbf{y}$$

$$\mathbf{y}^* = \arg\min_{\mathbf{y}} \max_{\mathbf{x}} \mathbf{x}^\top A \mathbf{y}$$

This is the minimax theorem (von Neumann, 1928):

$$\max_{\mathbf{x}} \min_{\mathbf{y}} \mathbf{x}^\top A \mathbf{y} = \min_{\mathbf{y}} \max_{\mathbf{x}} \mathbf{x}^\top A \mathbf{y} = v^*$$

where $v^*$ is the game value — the expected payoff at equilibrium.

Concrete Example

We model 5 infrastructure assets (e.g., Power Grid, Water System, Financial Network, Communication Hub, Transportation):

$$A = \begin{pmatrix} 3 & -1 & 0 & -2 & 1 \ -1 & 4 & -2 & 1 & -1 \ 2 & -1 & 5 & -3 & 0 \ -2 & 1 & -1 & 6 & -2 \ 1 & 0 & 2 & -1 & 3 \end{pmatrix}$$

Each entry $A_{ij}$ represents: positive = defender gains (attack repelled), negative = defender loses (attack succeeds).


Source Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
# ============================================================
# Game-Theoretic Attack Defense Strategy Optimization
# Nash Equilibrium via Linear Programming
# ============================================================

# --- Install required packages ---
import subprocess
subprocess.run(["pip", "install", "nashpy", "-q"], check=True)

import numpy as np
import nashpy as nash
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
from scipy.optimize import linprog
import warnings
warnings.filterwarnings("ignore")

# ============================================================
# 1. Define the Payoff Matrix
# ============================================================
asset_names = [
"Power Grid",
"Water System",
"Financial Net",
"Comm Hub",
"Transport"
]

# Defender payoff matrix (5x5)
# Positive: defender advantage | Negative: attacker advantage
A = np.array([
[ 3, -1, 0, -2, 1],
[-1, 4, -2, 1, -1],
[ 2, -1, 5, -3, 0],
[-2, 1, -1, 6, -2],
[ 1, 0, 2, -1, 3]
], dtype=float)

n_assets = len(asset_names)
print("=" * 55)
print(" Payoff Matrix A (Defender perspective)")
print("=" * 55)
header = f"{'':>14}" + "".join(f"{name:>14}" for name in asset_names)
print(f"{'[Attacker →]':>14}")
print(f"{'[Defender ↓]':>14}" + "".join(f"{name:>14}" for name in asset_names))
print("-" * (14 + 14 * n_assets))
for i, row_name in enumerate(asset_names):
row_str = "".join(f"{A[i,j]:>14.1f}" for j in range(n_assets))
print(f"{row_name:>14}{row_str}")
print()

# ============================================================
# 2. Solve Nash Equilibrium via Linear Programming (fast, robust)
# ============================================================

def solve_nash_lp(A):
"""
Solve zero-sum game Nash Equilibrium via Linear Programming.

Defender (row player) maximizes game value v:
maximize v
subject to:
A^T x >= v * 1 (each attacker strategy yields >= v)
sum(x) = 1
x >= 0

Rewrite as standard LP (minimize -v):
minimize -v (over [x_1,...,x_m, v])
subject to:
-A^T x + v*1 <= 0
sum(x) = 1
x >= 0, v unbounded
"""
m, n = A.shape

# Variables: [x_0, ..., x_{m-1}, v] (length m+1)
# Objective: minimize -v => c = [0,...,0, -1]
c = np.zeros(m + 1)
c[-1] = -1.0 # minimize -v <=> maximize v

# Inequality constraints: -A^T x + v <= 0
# For each attacker strategy j: sum_i(-A[i,j]*x[i]) + v <= 0
A_ub = np.zeros((n, m + 1))
for j in range(n):
A_ub[j, :m] = -A[:, j] # -A^T column j
A_ub[j, m] = 1.0 # +v
b_ub = np.zeros(n)

# Equality constraint: sum(x) = 1
A_eq = np.zeros((1, m + 1))
A_eq[0, :m] = 1.0
b_eq = np.array([1.0])

# Bounds: x_i >= 0 (unbounded v)
bounds = [(0, None)] * m + [(None, None)]

result = linprog(c, A_ub=A_ub, b_ub=b_ub,
A_eq=A_eq, b_eq=b_eq,
bounds=bounds, method="highs")

x_star = result.x[:m]
v_star = result.x[m]
return x_star, v_star


def solve_nash_lp_attacker(A):
"""
Attacker (column player) minimizes game value v:
minimize v
subject to:
A y <= v * 1
sum(y) = 1
y >= 0

Rewrite: minimize v over [y_0,...,y_{n-1}, v]
A y - v*1 <= 0 => [A | -1] [y; v] <= 0
"""
m, n = A.shape

c = np.zeros(n + 1)
c[-1] = 1.0 # minimize v

A_ub = np.zeros((m, n + 1))
for i in range(m):
A_ub[i, :n] = A[i, :] # A row i
A_ub[i, n] = -1.0 # -v
b_ub = np.zeros(m)

A_eq = np.zeros((1, n + 1))
A_eq[0, :n] = 1.0
b_eq = np.array([1.0])

bounds = [(0, None)] * n + [(None, None)]

result = linprog(c, A_ub=A_ub, b_ub=b_ub,
A_eq=A_eq, b_eq=b_eq,
bounds=bounds, method="highs")

y_star = result.x[:n]
v_star = result.x[n]
return y_star, v_star


# Solve for both players
x_star, v_defender = solve_nash_lp(A)
y_star, v_attacker = solve_nash_lp_attacker(A)
game_value = (v_defender + v_attacker) / 2.0 # should be equal

print("=" * 55)
print(" Nash Equilibrium — Mixed Strategies")
print("=" * 55)
print(f"\n Game Value (v*): {game_value:.4f}")
print(f"\n Defender Mixed Strategy x*:")
for i, name in enumerate(asset_names):
bar = "█" * int(x_star[i] * 40)
print(f" {name:>14}: {x_star[i]:.4f} {bar}")
print(f"\n Attacker Mixed Strategy y*:")
for j, name in enumerate(asset_names):
bar = "█" * int(y_star[j] * 40)
print(f" {name:>14}: {y_star[j]:.4f} {bar}")

# ============================================================
# 3. Verify Nash Equilibrium: Best Response Check
# ============================================================
def expected_payoff(x, y, A):
return x @ A @ y

ev_nash = expected_payoff(x_star, y_star, A)

# Check defender's incentive to deviate
def_payoffs = [expected_payoff(np.eye(len(x_star))[i], y_star, A)
for i in range(len(x_star))]
# Check attacker's incentive to deviate
att_payoffs = [expected_payoff(x_star, np.eye(len(y_star))[j], A)
for j in range(len(y_star))]

print(f"\n Expected payoff at Nash Eq: {ev_nash:.4f}")
print(f" Defender pure-strategy payoffs vs y*: "
f"{[round(p,3) for p in def_payoffs]}")
print(f" Attacker pure-strategy payoffs vs x*: "
f"{[round(p,3) for p in att_payoffs]}")
print(f"\n [Nash Check] All defender payoffs <= {ev_nash:.4f}? "
f"{all(p <= ev_nash + 1e-6 for p in def_payoffs)}")
print(f" [Nash Check] All attacker payoffs >= {ev_nash:.4f}? "
f"{all(p >= ev_nash - 1e-6 for p in att_payoffs)}")

# ============================================================
# 4. Sensitivity Analysis: Game Value vs. Budget Allocation
# ============================================================
# Simulate: what if defender boosts defense on one asset?
# Model: A_boosted[i,i] += delta (diagonal boost = self-protection)

deltas = np.linspace(0, 5, 50)
sensitivity = np.zeros((n_assets, len(deltas)))

for asset_idx in range(n_assets):
for k, delta in enumerate(deltas):
A_mod = A.copy()
A_mod[asset_idx, asset_idx] += delta
_, v = solve_nash_lp(A_mod)
sensitivity[asset_idx, k] = v

# ============================================================
# 5. VISUALIZATION
# ============================================================

fig = plt.figure(figsize=(22, 18))
fig.patch.set_facecolor("#0d0d1a")

colors = ["#00f5d4", "#f72585", "#fee440", "#4cc9f0", "#b5e48c"]
title_color = "#ffffff"
grid_color = "#2a2a4a"

# ---- Plot 1: Payoff Matrix Heatmap ----
ax1 = fig.add_subplot(3, 3, 1)
ax1.set_facecolor("#0d0d1a")
im = ax1.imshow(A, cmap="RdYlGn", aspect="auto", vmin=-4, vmax=7)
plt.colorbar(im, ax=ax1, fraction=0.046, pad=0.04)
ax1.set_xticks(range(n_assets))
ax1.set_xticklabels(asset_names, rotation=35, ha="right",
fontsize=7, color=title_color)
ax1.set_yticks(range(n_assets))
ax1.set_yticklabels(asset_names, fontsize=7, color=title_color)
for i in range(n_assets):
for j in range(n_assets):
ax1.text(j, i, f"{A[i,j]:.0f}", ha="center", va="center",
fontsize=9, color="black", fontweight="bold")
ax1.set_title("Payoff Matrix\n(Green=Defender wins)",
color=title_color, fontsize=9, pad=8)
ax1.tick_params(colors=title_color)
for spine in ax1.spines.values():
spine.set_edgecolor(grid_color)

# ---- Plot 2: Nash Equilibrium Mixed Strategies ----
ax2 = fig.add_subplot(3, 3, 2)
ax2.set_facecolor("#0d0d1a")
x_pos = np.arange(n_assets)
width = 0.35
bars1 = ax2.bar(x_pos - width/2, x_star, width, label="Defender x*",
color=colors[0], alpha=0.85, edgecolor="#ffffff", linewidth=0.5)
bars2 = ax2.bar(x_pos + width/2, y_star, width, label="Attacker y*",
color=colors[1], alpha=0.85, edgecolor="#ffffff", linewidth=0.5)
ax2.set_xticks(x_pos)
ax2.set_xticklabels(asset_names, rotation=35, ha="right",
fontsize=7, color=title_color)
ax2.set_ylabel("Probability", color=title_color, fontsize=8)
ax2.set_title(f"Nash Equilibrium Mixed Strategies\n(Game Value v* = {game_value:.3f})",
color=title_color, fontsize=9)
ax2.legend(fontsize=8, facecolor="#1a1a2e", edgecolor=grid_color,
labelcolor=title_color)
ax2.set_facecolor("#0d0d1a")
ax2.tick_params(colors=title_color)
ax2.yaxis.label.set_color(title_color)
ax2.set_ylim(0, max(max(x_star), max(y_star)) * 1.3)
for spine in ax2.spines.values():
spine.set_edgecolor(grid_color)
ax2.grid(axis="y", color=grid_color, linewidth=0.5)

# ---- Plot 3: Best Response Payoffs ----
ax3 = fig.add_subplot(3, 3, 3)
ax3.set_facecolor("#0d0d1a")
ax3.plot(asset_names, def_payoffs, "o-", color=colors[0],
linewidth=2, markersize=8, label="Defender pure payoff vs y*")
ax3.plot(asset_names, att_payoffs, "s-", color=colors[1],
linewidth=2, markersize=8, label="Attacker pure payoff vs x*")
ax3.axhline(game_value, color=colors[2], linewidth=1.5,
linestyle="--", label=f"Game Value v*={game_value:.3f}")
ax3.set_xticks(range(n_assets))
ax3.set_xticklabels(asset_names, rotation=35, ha="right",
fontsize=7, color=title_color)
ax3.set_ylabel("Expected Payoff", color=title_color, fontsize=8)
ax3.set_title("Best Response Analysis\n(Deviation Incentive Check)",
color=title_color, fontsize=9)
ax3.legend(fontsize=7, facecolor="#1a1a2e", edgecolor=grid_color,
labelcolor=title_color)
ax3.tick_params(colors=title_color)
for spine in ax3.spines.values():
spine.set_edgecolor(grid_color)
ax3.grid(color=grid_color, linewidth=0.5)

# ---- Plot 4: Sensitivity Analysis (2D) ----
ax4 = fig.add_subplot(3, 3, 4)
ax4.set_facecolor("#0d0d1a")
for i, name in enumerate(asset_names):
ax4.plot(deltas, sensitivity[i], color=colors[i],
linewidth=2, label=name)
ax4.set_xlabel("Defense Boost δ", color=title_color, fontsize=8)
ax4.set_ylabel("Game Value v*(δ)", color=title_color, fontsize=8)
ax4.set_title("Sensitivity: Game Value vs Defense Boost\n(Diagonal Reinforcement)",
color=title_color, fontsize=9)
ax4.legend(fontsize=7, facecolor="#1a1a2e", edgecolor=grid_color,
labelcolor=title_color)
ax4.tick_params(colors=title_color)
for spine in ax4.spines.values():
spine.set_edgecolor(grid_color)
ax4.grid(color=grid_color, linewidth=0.5)

# ---- Plot 5: Expected Payoff Surface (2-asset simplex) ----
# Fix 3 assets at their NE values; vary defender allocation between
# asset 0 (Power Grid) and asset 2 (Financial Net)
ax5 = fig.add_subplot(3, 3, 5)
ax5.set_facecolor("#0d0d1a")

alphas = np.linspace(0, 1, 60)
betas = np.linspace(0, 1, 60)
Z_2d = np.zeros((len(alphas), len(betas)))

for ia, alpha in enumerate(alphas):
for ib, beta in enumerate(betas):
# Defender: allocates alpha to asset0, beta to asset2,
# rest distributed proportionally among remaining
remaining = 1.0 - alpha - beta
if remaining < 0:
Z_2d[ia, ib] = np.nan
continue
x_test = np.array([
alpha,
remaining * x_star[1] / (x_star[1] + x_star[3] + x_star[4] + 1e-9),
beta,
remaining * x_star[3] / (x_star[1] + x_star[3] + x_star[4] + 1e-9),
remaining * x_star[4] / (x_star[1] + x_star[3] + x_star[4] + 1e-9)
])
x_test = np.clip(x_test, 0, 1)
x_test /= x_test.sum()
Z_2d[ia, ib] = expected_payoff(x_test, y_star, A)

im5 = ax5.contourf(alphas, betas, Z_2d, levels=20,
cmap="plasma")
plt.colorbar(im5, ax=ax5, fraction=0.046, pad=0.04)
ax5.contour(alphas, betas, Z_2d, levels=10,
colors="white", linewidths=0.3, alpha=0.4)
ax5.plot(x_star[0], x_star[2], "w*", markersize=15,
label=f"Nash Eq x*")
ax5.set_xlabel("α: Power Grid allocation", color=title_color, fontsize=8)
ax5.set_ylabel("β: Financial Net allocation", color=title_color, fontsize=8)
ax5.set_title("Expected Payoff Contour\n(Varying 2-asset defense allocation)",
color=title_color, fontsize=9)
ax5.legend(fontsize=8, facecolor="#1a1a2e", edgecolor=grid_color,
labelcolor=title_color)
ax5.tick_params(colors=title_color)
for spine in ax5.spines.values():
spine.set_edgecolor(grid_color)

# ---- Plot 6: Risk Map (Asset criticality) ----
ax6 = fig.add_subplot(3, 3, 6)
ax6.set_facecolor("#0d0d1a")
loss_if_attacked = []
for j in range(n_assets):
# Expected loss = attacker targets j with certainty vs NE defender
loss_if_attacked.append(expected_payoff(x_star,
np.eye(n_assets)[j], A))

scatter_c = [colors[i % len(colors)] for i in range(n_assets)]
sc = ax6.scatter(y_star, x_star,
s=[abs(v)*200+80 for v in loss_if_attacked],
c=loss_if_attacked, cmap="RdYlGn",
vmin=min(loss_if_attacked), vmax=max(loss_if_attacked),
edgecolors="white", linewidth=1.2, zorder=3)
plt.colorbar(sc, ax=ax6, fraction=0.046, pad=0.04,
label="Payoff if attacked")
for i, name in enumerate(asset_names):
ax6.annotate(name, (y_star[i], x_star[i]),
textcoords="offset points", xytext=(6, 4),
fontsize=7, color=title_color)
ax6.set_xlabel("Attacker prob y*", color=title_color, fontsize=8)
ax6.set_ylabel("Defender prob x*", color=title_color, fontsize=8)
ax6.set_title("Strategic Risk Map\n(Bubble size ∝ |payoff if attacked|)",
color=title_color, fontsize=9)
ax6.tick_params(colors=title_color)
for spine in ax6.spines.values():
spine.set_edgecolor(grid_color)
ax6.grid(color=grid_color, linewidth=0.5)

# ---- Plot 7: 3D Payoff Surface (alpha vs beta vs payoff) ----
ax7 = fig.add_subplot(3, 3, 7, projection="3d")
ax7.set_facecolor("#0d0d1a")

Alpha_g, Beta_g = np.meshgrid(alphas, betas)
Z_masked = np.where(Alpha_g + Beta_g <= 1.0, Z_2d.T, np.nan)

surf = ax7.plot_surface(Alpha_g, Beta_g, Z_masked,
cmap="plasma", alpha=0.85,
linewidth=0, antialiased=True)
ax7.scatter([x_star[0]], [x_star[2]], [ev_nash],
s=120, c="white", marker="*", zorder=10,
label="Nash Eq")
ax7.set_xlabel("α (Power Grid)", color=title_color, fontsize=7, labelpad=6)
ax7.set_ylabel("β (Financial Net)", color=title_color, fontsize=7, labelpad=6)
ax7.set_zlabel("Expected Payoff", color=title_color, fontsize=7, labelpad=6)
ax7.set_title("3D Payoff Surface\n(Defender 2-asset allocation)",
color=title_color, fontsize=9, pad=10)
ax7.tick_params(colors=title_color, labelsize=6)
ax7.xaxis.pane.fill = False
ax7.yaxis.pane.fill = False
ax7.zaxis.pane.fill = False
ax7.xaxis.pane.set_edgecolor(grid_color)
ax7.yaxis.pane.set_edgecolor(grid_color)
ax7.zaxis.pane.set_edgecolor(grid_color)
ax7.grid(color=grid_color, linewidth=0.3)
ax7.legend(fontsize=7, facecolor="#0d0d1a", edgecolor=grid_color,
labelcolor=title_color)

# ---- Plot 8: 3D Sensitivity Surface ----
ax8 = fig.add_subplot(3, 3, 8, projection="3d")
ax8.set_facecolor("#0d0d1a")

D_grid, Asset_grid = np.meshgrid(deltas, np.arange(n_assets))
surf2 = ax8.plot_surface(D_grid, Asset_grid, sensitivity,
cmap="viridis", alpha=0.88,
linewidth=0, antialiased=True)
ax8.set_xlabel("Defense Boost δ", color=title_color, fontsize=7, labelpad=6)
ax8.set_ylabel("Asset Index", color=title_color, fontsize=7, labelpad=6)
ax8.set_zlabel("Game Value v*", color=title_color, fontsize=7, labelpad=6)
ax8.set_yticks(range(n_assets))
ax8.set_yticklabels([n[:5] for n in asset_names],
fontsize=5, color=title_color)
ax8.set_title("3D Sensitivity Surface\n(v* vs. which asset gets boosted)",
color=title_color, fontsize=9, pad=10)
ax8.tick_params(colors=title_color, labelsize=6)
ax8.xaxis.pane.fill = False
ax8.yaxis.pane.fill = False
ax8.zaxis.pane.fill = False
ax8.xaxis.pane.set_edgecolor(grid_color)
ax8.yaxis.pane.set_edgecolor(grid_color)
ax8.zaxis.pane.set_edgecolor(grid_color)
ax8.grid(color=grid_color, linewidth=0.3)

# ---- Plot 9: Strategy Evolution (Fictitious Play simulation) ----
ax9 = fig.add_subplot(3, 3, 9)
ax9.set_facecolor("#0d0d1a")

np.random.seed(42)
n_iter = 800
def_counts = np.ones(n_assets)
att_counts = np.ones(n_assets)
def_hist = np.zeros((n_iter, n_assets))
att_hist = np.zeros((n_iter, n_assets))

for t in range(n_iter):
x_t = def_counts / def_counts.sum()
y_t = att_counts / att_counts.sum()
# Best response: each player picks best pure strategy
def_br = np.argmax(A @ y_t)
att_br = np.argmin(A.T @ x_t)
def_counts[def_br] += 1
att_counts[att_br] += 1
def_hist[t] = def_counts / def_counts.sum()
att_hist[t] = att_counts / att_counts.sum()

iters = np.arange(n_iter)
for i in range(n_assets):
ax9.plot(iters, def_hist[:, i], color=colors[i],
linewidth=1.5, alpha=0.9, label=asset_names[i])
ax9.axhline(x_star[i], color=colors[i],
linewidth=1, linestyle=":", alpha=0.6)
ax9.set_xlabel("Iteration", color=title_color, fontsize=8)
ax9.set_ylabel("Mixed Strategy Prob.", color=title_color, fontsize=8)
ax9.set_title("Fictitious Play Convergence\n(Dotted = Nash Eq value)",
color=title_color, fontsize=9)
ax9.legend(fontsize=6, facecolor="#1a1a2e", edgecolor=grid_color,
labelcolor=title_color, ncol=2)
ax9.tick_params(colors=title_color)
for spine in ax9.spines.values():
spine.set_edgecolor(grid_color)
ax9.grid(color=grid_color, linewidth=0.5)

plt.suptitle(
"Game-Theoretic Attack Defense Optimization — Nash Equilibrium Analysis",
color=title_color, fontsize=14, fontweight="bold", y=1.01
)
plt.tight_layout(rect=[0, 0, 1, 1])
plt.savefig("game_theory_defense.png", dpi=150, bbox_inches="tight",
facecolor="#0d0d1a")
plt.show()
print("\n[Figure saved as game_theory_defense.png]")

Code Walkthrough

Section 1 — Payoff Matrix Definition

The $5 \times 5$ matrix A encodes the strategic interaction. Each row is a defender asset, each column is an attacker target. The sign convention:

  • $A_{ij} > 0$: attack on asset $j$ while defending $i$ favors the defender
  • $A_{ij} < 0$: attack on asset $j$ while defending $i$ favors the attacker

Section 2 — Nash Equilibrium via Linear Programming

Rather than using support enumeration (which can be slow and numerically fragile), we solve the Nash Equilibrium directly via LP duality. The defender’s problem:

$$\max_{v,, \mathbf{x}} ; v \quad \text{s.t.} \quad \mathbf{x}^\top A_j \geq v ; \forall j, \quad \sum_i x_i = 1, \quad x_i \geq 0$$

This is cast as a standard-form LP and solved by HiGHS (the fastest open-source LP solver, available in scipy >= 1.9). The attacker’s symmetric LP gives $\mathbf{y}^*$.

Why LP and not nashpy’s support enumeration?
nashpy enumerates all support pairs — exponential worst case. For $5 \times 5$, it’s fine, but LP is $O(m \cdot n)$ and scales to much larger games.

Section 3 — Nash Equilibrium Verification

We verify the equilibrium conditions:

  • Defender: no pure strategy improves expected payoff beyond $v^*$ (given $\mathbf{y}^*$)
  • Attacker: no pure strategy reduces expected payoff below $v^*$ (given $\mathbf{x}^*$)

This is the no-deviation condition that defines Nash Equilibrium.

Section 4 — Sensitivity Analysis

We simulate a diagonal boost: $A_{ii} \leftarrow A_{ii} + \delta$. This models the defender investing additional resources specifically to protect asset $i$ (reducing that asset’s attack loss). We recompute the LP for each $(\text{asset}, \delta)$ pair — 250 LP solves total, finishing in under a second thanks to HiGHS.

Section 5 — Visualization (9 panels)

Panel What it shows
Payoff Matrix Heatmap Raw strategic values — green cells favor defender, red favors attacker
Nash Mixed Strategies Side-by-side bar chart of $\mathbf{x}^*$ and $\mathbf{y}^*$
Best Response Analysis Confirms no player has incentive to deviate unilaterally
Sensitivity 2D How game value rises when each asset gets reinforced
Payoff Contour 2D slice of defender’s strategy space with NE marked
Strategic Risk Map Bubble chart: attack prob vs. defense prob, bubble = criticality
3D Payoff Surface Full landscape of defender payoffs over a 2-asset simplex
3D Sensitivity Surface Game value over (asset, boost) domain
Fictitious Play Learning dynamics converging to Nash Equilibrium

Fictitious Play (Panel 9)

Fictitious Play is a classic learning algorithm: at each step, each player best-responds to the empirical frequency of the opponent’s past play. The dotted horizontal lines are the Nash Equilibrium values — the empirical averages provably converge to $\mathbf{x}^*$ in zero-sum games (Robinson, 1951).


Results and Interpretation

=======================================================
  Payoff Matrix A (Defender perspective)
=======================================================
  [Attacker →]
  [Defender ↓]    Power Grid  Water System Financial Net      Comm Hub     Transport
------------------------------------------------------------------------------------
    Power Grid           3.0          -1.0           0.0          -2.0           1.0
  Water System          -1.0           4.0          -2.0           1.0          -1.0
 Financial Net           2.0          -1.0           5.0          -3.0           0.0
      Comm Hub          -2.0           1.0          -1.0           6.0          -2.0
     Transport           1.0           0.0           2.0          -1.0           3.0

=======================================================
  Nash Equilibrium — Mixed Strategies
=======================================================

  Game Value (v*): 0.5288

  Defender Mixed Strategy x*:
        Power Grid: 0.2190  ████████
      Water System: 0.1513  ██████
     Financial Net: 0.0893  ███
          Comm Hub: 0.2320  █████████
         Transport: 0.3084  ████████████

  Attacker Mixed Strategy y*:
        Power Grid: 0.3862  ███████████████
      Water System: 0.2478  █████████
     Financial Net: 0.1254  █████
          Comm Hub: 0.2075  ████████
         Transport: 0.0331  █

  Expected payoff at Nash Eq: 0.5288
  Defender pure-strategy payoffs vs y*: [np.float64(0.529), np.float64(0.529), np.float64(0.529), np.float64(0.529), np.float64(0.529)]
  Attacker pure-strategy payoffs vs x*: [np.float64(0.529), np.float64(0.529), np.float64(0.529), np.float64(0.529), np.float64(0.529)]

  [Nash Check] All defender payoffs <= 0.5288? True
  [Nash Check] All attacker payoffs >= 0.5288? True

[Figure saved as game_theory_defense.png]

Key Insights

1. Mixed Strategies Are Essential

In this game, no pure strategy is a Nash Equilibrium. If the defender always protects the Comm Hub (the highest single diagonal value), the attacker simply concentrates on everything else. Randomization — following $\mathbf{x}^*$ — prevents exploitation.

2. The Game Value Is the Strategic Anchor

The Nash game value $v^* \approx$ (your result) is the guaranteed minimum the defender can achieve regardless of attacker strategy. No other defender strategy guarantees more.

3. Sensitivity Reveals Investment Priority

From the sensitivity surface, the asset whose diagonal boost produces the steepest rise in $v^*$ is the most strategically leveraged investment. Reinforcing assets that the attacker is already unlikely to target wastes budget.

4. Fictitious Play Confirms Robustness

The convergence in Panel 9 demonstrates that even without solving the LP explicitly — just through repeated best-responding — the strategies naturally drift to the Nash Equilibrium. This validates the equilibrium as the rational outcome of adaptive adversarial dynamics.


Conclusion

Game-theoretic equilibrium analysis gives defenders a principled, mathematically grounded framework for resource allocation against strategic adversaries. The key takeaways:

  • Solve the zero-sum LP for fast, exact Nash Equilibria
  • The game value $v^*$ is your security guarantee
  • Sensitivity analysis guides where to spend additional budget
  • Fictitious Play shows that rational learning dynamics converge to the equilibrium naturally

This approach applies broadly: cybersecurity, military resource allocation, counter-terrorism, even competitive pricing — anywhere a rational adversary exists.