diff --git a/game/monsters.py b/game/monsters.py index 96b98a8..2ba8ea6 100644 --- a/game/monsters.py +++ b/game/monsters.py @@ -57,7 +57,32 @@ def spawn(self, x: int, y: int) -> Actor: ] -def eligible_monsters(depth: int) -> list[MonsterType]: - min_level = max(1, min(depth - 5, len(monsters) - 4)) - max_level = max(5, min(depth + 4, len(monsters))) - return [m for m in monsters[min_level - 1:max_level] if m.generate] +def get_monster_types(depth: int) -> tuple[list[MonsterType], list[int]]: + items = [] + weights = [] + for i, weight in enumerate(_weights(depth)): + if weight > 0 and monsters[i].generate: + items.append(monsters[i]) + weights.append(weight) + return items, weights + + +def _weights(depth: int) -> list[int]: + assert depth >= 1 + weights = [0] * 26 + under = 0 + over = 0 + for i in range(depth - 6, depth + 4): + if i < 0: + under += 10 + elif i >= len(weights): + over += 10 + else: + weights[i] += 10 + for i in range(0, 5): + weights[i] += under // 5 + for i in range(21, 26): + weights[i] += over // 5 + assert len(weights) == 26 + assert sum(weights) == 100 + return weights diff --git a/game/procgen.py b/game/procgen.py index 3ee0146..c36a728 100644 --- a/game/procgen.py +++ b/game/procgen.py @@ -7,7 +7,7 @@ from game.entity import ArmorItem, Item, WeaponItem from game.items import get_item_categories from game.level import Level -from game.monsters import eligible_monsters +from game.monsters import get_monster_types # hardcoded 80x22 subdivision H_GRID = [0, 26, 27, 53, 54, 80] @@ -247,8 +247,9 @@ def place_gold(room: Room, level: Level) -> None: def place_monster(room: Room, level: Level) -> None: x, y = find_empty_spot_in_room(room, level) - m_type = rng.choice(eligible_monsters(level.depth)) - monster = m_type.spawn(x, y) + monster_types, weights = get_monster_types(level.depth) + monster_type = rng.choices(monster_types, weights).pop() + monster = monster_type.spawn(x, y) level.entities.add(monster) diff --git a/tests/test_monsters_weights.py b/tests/test_monsters_weights.py new file mode 100644 index 0000000..1fa323b --- /dev/null +++ b/tests/test_monsters_weights.py @@ -0,0 +1,66 @@ +import pytest + +import game.monsters + + +@pytest.mark.skip(reason="Enable to dump the full table.") +def test_dump_weights(): + print() + for d in range(1, 33): + weights = game.monsters._weights(d) + print(" ".join(f"{item:2d}" for item in weights)) + + +def test_level_1(): + weights = game.monsters._weights(1) + assert weights == [20] * 5 + [0] * 21 + + +def test_level_2_to_5(): + for d in range(2, 6): + weights = game.monsters._weights(d) + assert weights == [22 - 2 * d] * 5 + [10] * (d - 1) + [0] * (22 - d) + + +def test_level_6(): + weights = game.monsters._weights(6) + assert weights == [10] * 10 + [0] * 16 + + +def test_level_7_to_21(): + for d in range(7, 22): + weights = game.monsters._weights(d) + assert weights == [0] * (d - 6) + [10] * 10 + [0] * (22 - d) + + +def test_level_22(): + weights = game.monsters._weights(22) + assert weights == [0] * 16 + [10] * 10 + + +def test_level_23_to_26(): + for d in range(23, 27): + weights = game.monsters._weights(d) + assert weights == [0] * (d - 6) + [10] * (27 - d) + [2 * d - 34] * 5 + + +def test_level_27(): + weights = game.monsters._weights(27) + assert weights == [0] * 21 + [20] * 5 + + +def test_level_28_to_31(): + for d in range(28, 32): + weights = game.monsters._weights(d) + assert weights == [0] * 21 + [2 * d - 44] * (d - 27) + [2 * d - 34] * (32 - d) + + +def test_level_32(): + weights = game.monsters._weights(32) + assert weights == [0] * 21 + [20] * 5 + + +def test_level_33_and_beyond(): + for d in range(33, 100): + weights = game.monsters._weights(d) + assert weights == [0] * 21 + [20] * 5