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