aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/tests/test_conflict.py
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 00:01:14 +0200
committergodosa <godosa@godosa.eu>2026-10-07 00:01:14 +0200
commited1dea2639b1191421de3986483aedcc14067a12 (patch)
tree0118c6e119a84f1a9433ed0a304c8be1c9098463 /tests/test_conflict.py
downloadworldhistory-ed1dea2639b1191421de3986483aedcc14067a12.tar.gz
worldhistory-ed1dea2639b1191421de3986483aedcc14067a12.zip
worldhistory: initial public history
Diffstat (limited to 'tests/test_conflict.py')
-rw-r--r--tests/test_conflict.py77
1 files changed, 77 insertions, 0 deletions
diff --git a/tests/test_conflict.py b/tests/test_conflict.py
new file mode 100644
index 0000000..7fd7f6e
--- /dev/null
+++ b/tests/test_conflict.py
@@ -0,0 +1,77 @@
+import unittest
+
+import numpy as np
+
+from tests.helpers import make_race
+from worldhistory.conflict import apply_conflict, apply_curse, conflict
+from worldhistory.state import new_state
+
+
+def two(ca, cb):
+ return [make_race("a", conflict=ca), make_race("b", conflict=cb)]
+
+
+class ConflictTest(unittest.TestCase):
+ def test_internal_scales_with_crowding(self):
+ r = [make_race("a", conflict={"internal": 0.01})]
+ loss, _ = conflict(np.array([[100.0, 100.0]]), np.ones((1, 2)), np.array([[0.5, 1.0]]), r)
+ np.testing.assert_allclose(loss[0], [0.5, 1.0])
+
+ def test_no_border_losses_alone_or_within_family(self):
+ r = two({"border": 0.1}, {"border": 0.1})
+ r[1].family = "a"
+ loss, press = conflict(np.array([[50.0], [50.0]]), np.ones((2, 1)), np.zeros((2, 1)), r)
+ np.testing.assert_array_equal(loss, 0)
+ np.testing.assert_array_equal(press, 0)
+
+ def test_warlike_neighbour_hurts_more(self):
+ P = np.array([[50.0], [50.0]])
+ r = two({"border": 0.1, "aggression": 0.1}, {"border": 0.1, "aggression": 0.9})
+ loss, press = conflict(P, np.zeros((2, 1)), np.zeros((2, 1)), r)
+ self.assertGreater(loss[0, 0], loss[1, 0])
+ self.assertGreater(press[0, 0], press[1, 0])
+ # a: attacked = 0.9*1/1*0.5 = 0.45; attacking b outside b's core = 0 -> loss 0.1*0.45*50
+ self.assertAlmostEqual(float(loss[0, 0]), 0.1 * 0.45 * 50)
+
+ def test_defend_in_core_raises_attacker_losses(self):
+ P = np.array([[50.0], [50.0]])
+ base = two({"border": 0.1, "aggression": 0.8}, {"border": 0.1, "defend": 1.0})
+ hard = two({"border": 0.1, "aggression": 0.8}, {"border": 0.1, "defend": 3.0})
+ core = np.array([[0.0], [1.0]]) # the cell is b's core habitat
+ l1, _ = conflict(P, core, np.zeros((2, 1)), base)
+ l3, _ = conflict(P, core, np.zeros((2, 1)), hard)
+ self.assertGreater(l3[0, 0], l1[0, 0])
+
+ def test_yield_turns_losses_into_moves(self):
+ st = new_state(1, 1, np.zeros((1, 6)), seed=0)
+ st.P[0, 0] = 100.0
+ apply_conflict(st, np.array([[10.0]]), [make_race(conflict={"yield": 0.8})])
+ self.assertAlmostEqual(float(st.P[0, 0]), 98.0)
+ self.assertAlmostEqual(float(st.push[0, 0]), 8.0)
+
+
+class DreadCurseTest(unittest.TestCase):
+ def test_dread_deters_attacks_and_repels(self):
+ P = np.array([[50.0], [50.0]])
+ plain = two({"border": 0.1}, {"border": 0.1, "aggression": 0.9})
+ feared = two({"border": 0.1, "dread": 0.8}, {"border": 0.1, "aggression": 0.9})
+ l0, p0 = conflict(P, np.zeros((2, 1)), np.zeros((2, 1)), plain)
+ l1, p1 = conflict(P, np.zeros((2, 1)), np.zeros((2, 1)), feared)
+ self.assertAlmostEqual(float(l1[0, 0]), 0.2 * float(l0[0, 0])) # b's attacks on a × (1 − 0.8)
+ self.assertAlmostEqual(float(p1[1, 0]), float(p0[1, 0]) + 0.8 * 0.5) # b is repelled by a's dread × a's share
+
+ def test_blame_falls_on_attackers_of_cursing_race(self):
+ P = np.array([[50.0], [50.0]])
+ r = two({"border": 0.1, "curse": 2.0}, {"border": 0.1, "aggression": 0.9})
+ blame = np.zeros((2, 1))
+ conflict(P, np.zeros((2, 1)), np.zeros((2, 1)), r, blame=blame)
+ # share of a's clan killed by b = border 0.1 × attacked 0.9·1·0.5 = 0.045; × curse 2
+ np.testing.assert_allclose(blame[:, 0], [0.0, 0.09])
+
+ def test_curse_kills_and_fades(self):
+ st = new_state(1, 2, np.zeros((1, 6)), seed=0)
+ st.P[0] = [100.0, 100.0]
+ apply_curse(st, np.array([[0.1, 0.0]]), decay=0.5)
+ np.testing.assert_allclose(st.P[0], [90.0, 100.0])
+ apply_curse(st, np.zeros((1, 2)), decay=0.5)
+ np.testing.assert_allclose(st.P[0], [85.5, 100.0]) # curse halves: 5 % this step