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