SC2: Content update (#5312)

Feature highlights:
- Adds many content to the SC2 game
- Allows custom mission order
- Adds race-swapped missions for build missions (except Epilogue and NCO)
- Allows War Council Nerfs (Protoss units can get pre - War Council State, alternative units get another custom nerf to match the power level of base units)
- Revamps Predator's upgrade tree (never was considered strategically important)
- Adds some units and upgrades
- Locked and excluded items can specify quantity
- Key mode (if opt-in, missions require keys to be unlocked on top of their regular regular requirements
- Victory caches - Victory locations can grant multiple items to the multiworld instead of one 
- The generator is more resilient for generator failures as it validates logic for item excludes
- Fixes the following issues:
  - https://github.com/ArchipelagoMW/Archipelago/issues/3531 
  - https://github.com/ArchipelagoMW/Archipelago/issues/3548
This commit is contained in:
Ziktofel
2025-09-02 17:40:58 +02:00
committed by GitHub
parent 2359cceb64
commit 5f1835c546
73 changed files with 46368 additions and 13655 deletions

View File

@@ -1,41 +0,0 @@
import unittest
from .test_base import Sc2TestBase
from .. import Regions
from .. import Options, MissionTables
class TestGridsizes(unittest.TestCase):
def test_grid_sizes_meet_specs(self):
self.assertTupleEqual((1, 2, 0), Regions.get_grid_dimensions(2))
self.assertTupleEqual((1, 3, 0), Regions.get_grid_dimensions(3))
self.assertTupleEqual((2, 2, 0), Regions.get_grid_dimensions(4))
self.assertTupleEqual((2, 3, 1), Regions.get_grid_dimensions(5))
self.assertTupleEqual((2, 4, 1), Regions.get_grid_dimensions(7))
self.assertTupleEqual((2, 4, 0), Regions.get_grid_dimensions(8))
self.assertTupleEqual((3, 3, 0), Regions.get_grid_dimensions(9))
self.assertTupleEqual((2, 5, 0), Regions.get_grid_dimensions(10))
self.assertTupleEqual((3, 4, 1), Regions.get_grid_dimensions(11))
self.assertTupleEqual((3, 4, 0), Regions.get_grid_dimensions(12))
self.assertTupleEqual((3, 5, 0), Regions.get_grid_dimensions(15))
self.assertTupleEqual((4, 4, 0), Regions.get_grid_dimensions(16))
self.assertTupleEqual((4, 6, 0), Regions.get_grid_dimensions(24))
self.assertTupleEqual((5, 5, 0), Regions.get_grid_dimensions(25))
self.assertTupleEqual((5, 6, 1), Regions.get_grid_dimensions(29))
self.assertTupleEqual((5, 7, 2), Regions.get_grid_dimensions(33))
class TestGridGeneration(Sc2TestBase):
options = {
"mission_order": Options.MissionOrder.option_grid,
"excluded_missions": [MissionTables.SC2Mission.ZERO_HOUR.mission_name,],
"enable_hots_missions": False,
"enable_prophecy_missions": True,
"enable_lotv_prologue_missions": False,
"enable_lotv_missions": False,
"enable_epilogue_missions": False,
"enable_nco_missions": False
}
def test_size_matches_exclusions(self):
self.assertNotIn(MissionTables.SC2Mission.ZERO_HOUR.mission_name, self.multiworld.regions)
# WoL has 29 missions. -1 for Zero Hour being excluded, +1 for the automatically-added menu location
self.assertEqual(len(self.multiworld.regions), 29)

View File

@@ -1,11 +1,52 @@
from typing import *
import unittest
import random
from argparse import Namespace
from BaseClasses import MultiWorld, CollectionState, PlandoOptions
from Generate import get_seed_name
from worlds import AutoWorld
from test.general import gen_steps, call_all
from test.TestBase import WorldTestBase
from test.bases import WorldTestBase
from .. import SC2World
from .. import Client
from .. import client
class Sc2TestBase(WorldTestBase):
game = Client.SC2Context.game
game = client.SC2Context.game
world: SC2World
player: ClassVar[int] = 1
skip_long_tests: bool = True
class Sc2SetupTestBase(unittest.TestCase):
"""
A custom sc2-specific test base class that provides an explicit function to generate the world from options.
This allows potentially generating multiple worlds in one test case, useful for tracking down a rare / sporadic
crash.
"""
seed: Optional[int] = None
game = SC2World.game
player = 1
def generate_world(self, options: Dict[str, Any]) -> None:
self.multiworld = MultiWorld(1)
self.multiworld.game[self.player] = self.game
self.multiworld.player_name = {self.player: "Tester"}
self.multiworld.set_seed(self.seed)
random.seed(self.multiworld.seed)
self.multiworld.seed_name = get_seed_name(random) # only called to get same RNG progression as Generate.py
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items():
new_option = option.from_any(options.get(name, option.default))
new_option.verify(SC2World, "Tester", PlandoOptions.items|PlandoOptions.connections|PlandoOptions.texts|PlandoOptions.bosses)
setattr(args, name, {
1: new_option
})
self.multiworld.set_options(args)
self.world: SC2World = cast(SC2World, self.multiworld.worlds[self.player])
self.multiworld.state = CollectionState(self.multiworld)
try:
for step in gen_steps:
call_all(self.multiworld, step)
except Exception as ex:
ex.add_note(f"Seed: {self.multiworld.seed}")
raise

View File

@@ -0,0 +1,216 @@
"""
Unit tests for custom mission orders
"""
from .test_base import Sc2SetupTestBase
from .. import MissionFlag
from ..item import item_tables, item_names
from BaseClasses import ItemClassification
class TestCustomMissionOrders(Sc2SetupTestBase):
def test_mini_wol_generates(self):
world_options = {
'mission_order': 'custom',
'custom_mission_order': {
'Mini Wings of Liberty': {
'global': {
'type': 'column',
'mission_pool': [
'terran missions',
'^ wol missions'
]
},
'Mar Sara': {
'size': 1
},
'Colonist': {
'size': 2,
'entry_rules': [{
'scope': '../Mar Sara'
}]
},
'Artifact': {
'size': 3,
'entry_rules': [{
'scope': '../Mar Sara'
}],
'missions': [
{
'index': 1,
'entry_rules': [{
'scope': 'Mini Wings of Liberty',
'amount': 4
}]
},
{
'index': 2,
'entry_rules': [{
'scope': 'Mini Wings of Liberty',
'amount': 8
}]
}
]
},
'Prophecy': {
'size': 2,
'entry_rules': [{
'scope': '../Artifact/1'
}],
'mission_pool': [
'protoss missions',
'^ prophecy missions'
]
},
'Covert': {
'size': 2,
'entry_rules': [{
'scope': 'Mini Wings of Liberty',
'amount': 2
}]
},
'Rebellion': {
'size': 2,
'entry_rules': [{
'scope': 'Mini Wings of Liberty',
'amount': 3
}]
},
'Char': {
'size': 3,
'entry_rules': [{
'scope': '../Artifact/2'
}],
'missions': [
{
'index': 0,
'next': [2]
},
{
'index': 1,
'entrance': True
}
]
}
}
}
}
self.generate_world(world_options)
flags = self.world.custom_mission_order.get_used_flags()
self.assertEqual(flags[MissionFlag.Terran], 13)
self.assertEqual(flags[MissionFlag.Protoss], 2)
self.assertEqual(flags.get(MissionFlag.Zerg, 0), 0)
sc2_regions = set(self.multiworld.regions.region_cache[self.player]) - {"Menu"}
self.assertEqual(len(self.world.custom_mission_order.get_used_missions()), len(sc2_regions))
def test_locked_and_necessary_item_appears_once(self):
# This is a filler upgrade with a parent
test_item = item_names.MARINE_OPTIMIZED_LOGISTICS
world_options = {
'mission_order': 'custom',
'locked_items': { test_item: 1 },
'custom_mission_order': {
'test': {
'type': 'column',
'size': 5, # Give the generator some space to place the key
'max_difficulty': 'easy',
'missions': [{
'index': 4,
'entry_rules': [{
'items': { test_item: 1 }
}]
}]
}
}
}
self.assertNotEqual(item_tables.item_table[test_item].classification, ItemClassification.progression, f"Test item {test_item} won't change classification")
self.generate_world(world_options)
test_items_in_pool = [item for item in self.multiworld.itempool if item.name == test_item]
test_items_in_pool += [item for item in self.multiworld.precollected_items[self.player] if item.name == test_item]
self.assertEqual(len(test_items_in_pool), 1)
self.assertEqual(test_items_in_pool[0].classification, ItemClassification.progression)
def test_start_inventory_and_necessary_item_appears_once(self):
# This is a filler upgrade with a parent
test_item = item_names.ZERGLING_METABOLIC_BOOST
world_options = {
'mission_order': 'custom',
'start_inventory': { test_item: 1 },
'custom_mission_order': {
'test': {
'type': 'column',
'size': 5, # Give the generator some space to place the key
'max_difficulty': 'easy',
'missions': [{
'index': 4,
'entry_rules': [{
'items': { test_item: 1 }
}]
}]
}
}
}
self.generate_world(world_options)
test_items_in_pool = [item for item in self.multiworld.itempool if item.name == test_item]
self.assertEqual(len(test_items_in_pool), 0)
test_items_in_start_inventory = [item for item in self.multiworld.precollected_items[self.player] if item.name == test_item]
self.assertEqual(len(test_items_in_start_inventory), 1)
def test_start_inventory_and_locked_and_necessary_item_appears_once(self):
# This is a filler upgrade with a parent
test_item = item_names.ZERGLING_METABOLIC_BOOST
world_options = {
'mission_order': 'custom',
'start_inventory': { test_item: 1 },
'locked_items': { test_item: 1 },
'custom_mission_order': {
'test': {
'type': 'column',
'size': 5, # Give the generator some space to place the key
'max_difficulty': 'easy',
'missions': [{
'index': 4,
'entry_rules': [{
'items': { test_item: 1 }
}]
}]
}
}
}
self.generate_world(world_options)
test_items_in_pool = [item for item in self.multiworld.itempool if item.name == test_item]
self.assertEqual(len(test_items_in_pool), 0)
test_items_in_start_inventory = [item for item in self.multiworld.precollected_items[self.player] if item.name == test_item]
self.assertEqual(len(test_items_in_start_inventory), 1)
def test_key_item_rule_creates_correct_item_amount(self):
# This is an item that normally only exists once
test_item = item_names.ZERGLING
test_amount = 3
world_options = {
'mission_order': 'custom',
'locked_items': { test_item: 1 }, # Make sure it is generated as normal
'custom_mission_order': {
'test': {
'type': 'column',
'size': 12, # Give the generator some space to place the keys
'max_difficulty': 'easy',
'mission_pool': ['zerg missions'], # Make sure the item isn't excluded by race selection
'missions': [{
'index': 10,
'entry_rules': [{
'items': { test_item: test_amount } # Require more than the usual item amount
}]
}]
}
}
}
self.generate_world(world_options)
test_items_in_pool = [item for item in self.multiworld.itempool if item.name == test_item]
test_items_in_start_inventory = [item for item in self.multiworld.precollected_items[self.player] if item.name == test_item]
self.assertEqual(len(test_items_in_pool + test_items_in_start_inventory), test_amount)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
"""
Unit tests for item filtering like pool_filter.py
"""
from .test_base import Sc2SetupTestBase
from ..item import item_groups, item_names
from .. import options
from ..mission_tables import SC2Race
class ItemFilterTests(Sc2SetupTestBase):
def test_excluding_all_barracks_units_excludes_infantry_upgrades(self) -> None:
world_options = {
'excluded_items': {
item_groups.ItemGroupNames.BARRACKS_UNITS: 0
},
'required_tactics': 'standard',
'min_number_of_upgrades': 1,
'selected_races': {
SC2Race.TERRAN.get_title()
},
'mission_order': 'grid',
}
self.generate_world(world_options)
self.assertTrue(self.multiworld.itempool)
races = {mission.race for mission in self.world.custom_mission_order.get_used_missions()}
self.assertIn(SC2Race.TERRAN, races)
self.assertNotIn(SC2Race.ZERG, races)
self.assertNotIn(SC2Race.PROTOSS, races)
itempool = [item.name for item in self.multiworld.itempool]
self.assertNotIn(item_names.MARINE, itempool)
self.assertNotIn(item_names.MARAUDER, itempool)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, itempool)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, itempool)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE, itempool)
def test_excluding_one_item_of_multi_parent_doesnt_filter_children(self) -> None:
world_options = {
'locked_items': {
item_names.SENTINEL: 1,
item_names.CENTURION: 1,
},
'excluded_items': {
item_names.ZEALOT: 1,
# Exclude more items to make space
item_names.WRATHWALKER: 1,
item_names.ENERGIZER: 1,
item_names.AVENGER: 1,
item_names.ARBITER: 1,
item_names.VOID_RAY: 1,
item_names.PULSAR: 1,
item_names.DESTROYER: 1,
item_names.DAWNBRINGER: 1,
},
'min_number_of_upgrades': 2,
'required_tactics': 'standard',
'selected_races': {
SC2Race.PROTOSS.get_title()
},
'mission_order': 'grid',
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
}
self.generate_world(world_options)
self.assertTrue(self.multiworld.itempool)
itempool = [item.name for item in self.multiworld.itempool]
self.assertIn(item_names.ZEALOT_SENTINEL_CENTURION_SHIELD_CAPACITY, itempool)
self.assertIn(item_names.ZEALOT_SENTINEL_CENTURION_LEG_ENHANCEMENTS, itempool)
def test_excluding_all_items_in_multiparent_excludes_child_items(self) -> None:
world_options = {
'excluded_items': {
item_names.ZEALOT: 1,
item_names.SENTINEL: 1,
item_names.CENTURION: 1,
},
'min_number_of_upgrades': 2,
'required_tactics': 'standard',
'selected_races': {
SC2Race.PROTOSS.get_title()
},
'mission_order': 'grid',
}
self.generate_world(world_options)
self.assertTrue(self.multiworld.itempool)
itempool = [item.name for item in self.multiworld.itempool]
self.assertNotIn(item_names.ZEALOT_SENTINEL_CENTURION_SHIELD_CAPACITY, itempool)
self.assertNotIn(item_names.ZEALOT_SENTINEL_CENTURION_LEG_ENHANCEMENTS, itempool)

View File

@@ -0,0 +1,18 @@
import unittest
from ..item import item_descriptions, item_tables
class TestItemDescriptions(unittest.TestCase):
def test_all_items_have_description(self) -> None:
for item_name in item_tables.item_table:
self.assertIn(item_name, item_descriptions.item_descriptions)
def test_all_descriptions_refer_to_item_and_end_in_dot(self) -> None:
for item_name, item_desc in item_descriptions.item_descriptions.items():
self.assertIn(item_name, item_tables.item_table)
self.assertEqual(item_desc.strip()[-1], '.', msg=f"{item_name}'s item description does not end in a '.': '{item_desc}'")
def test_item_descriptions_follow_single_space_after_period_style(self) -> None:
for item_name, item_desc in item_descriptions.item_descriptions.items():
self.assertNotIn('. ', item_desc, f"Double-space after period in description for {item_name}")

View File

@@ -0,0 +1,32 @@
"""
Unit tests for item_groups.py
"""
import unittest
from ..item import item_groups, item_tables
class ItemGroupsUnitTests(unittest.TestCase):
def test_all_production_structure_groups_capture_all_units(self) -> None:
self.assertCountEqual(
item_groups.terran_units,
item_groups.barracks_units + item_groups.factory_units + item_groups.starport_units + item_groups.terran_mercenaries
)
self.assertCountEqual(
item_groups.protoss_units,
item_groups.gateway_units + item_groups.robo_units + item_groups.stargate_units
)
def test_terran_original_progressive_group_fully_contained_in_wol_upgrades(self) -> None:
for item_name in item_groups.terran_original_progressive_upgrades:
self.assertIn(item_tables.item_table[item_name].type, (
item_tables.TerranItemType.Progressive, item_tables.TerranItemType.Progressive_2), f"{item_name} is not progressive")
self.assertIn(item_name, item_groups.wol_upgrades)
def test_all_items_in_stimpack_group_are_stimpacks(self) -> None:
for item_name in item_groups.terran_stimpacks:
self.assertIn("Stimpack", item_name)
def test_all_item_group_names_have_a_group_defined(self) -> None:
for display_name in item_groups.ItemGroupNames.get_all_group_names():
self.assertIn(display_name, item_groups.item_name_groups)

View File

@@ -0,0 +1,170 @@
import unittest
from typing import List, Set
from ..item import item_tables
class TestItems(unittest.TestCase):
def test_grouped_upgrades_number(self) -> None:
"""
Tests if grouped upgrades have set number correctly
"""
bundled_items = item_tables.upgrade_bundles.keys()
bundled_item_data = [item_tables.get_full_item_list()[item_name] for item_name in bundled_items]
bundled_item_numbers = [item_data.number for item_data in bundled_item_data]
check_numbers = [number == -1 for number in bundled_item_numbers]
self.assertNotIn(False, check_numbers)
def test_non_grouped_upgrades_number(self) -> None:
"""
Checks if non-grouped upgrades number is set correctly thus can be sent into the game.
"""
check_modulo = 4
bundled_items = item_tables.upgrade_bundles.keys()
non_bundled_upgrades = [
item_name for item_name in item_tables.get_full_item_list().keys()
if (item_name not in bundled_items
and item_tables.get_full_item_list()[item_name].type in item_tables.upgrade_item_types)
]
non_bundled_upgrade_data = [item_tables.get_full_item_list()[item_name] for item_name in non_bundled_upgrades]
non_bundled_upgrade_numbers = [item_data.number for item_data in non_bundled_upgrade_data]
check_numbers = [number % check_modulo == 0 for number in non_bundled_upgrade_numbers]
self.assertNotIn(False, check_numbers)
def test_bundles_contain_only_basic_elements(self) -> None:
"""
Checks if there are no bundles within bundles.
"""
bundled_items = item_tables.upgrade_bundles.keys()
bundle_elements: List[str] = [item_name for values in item_tables.upgrade_bundles.values() for item_name in values]
for element in bundle_elements:
self.assertNotIn(element, bundled_items)
def test_weapon_armor_level(self) -> None:
"""
Checks if Weapon/Armor upgrade level is correctly set to all Weapon/Armor upgrade items.
"""
weapon_armor_upgrades = [item for item in item_tables.get_full_item_list() if item_tables.get_item_table()[item].type in item_tables.upgrade_item_types]
for weapon_armor_upgrade in weapon_armor_upgrades:
self.assertEqual(item_tables.get_full_item_list()[weapon_armor_upgrade].quantity, item_tables.WEAPON_ARMOR_UPGRADE_MAX_LEVEL)
def test_item_ids_distinct(self) -> None:
"""
Verifies if there are no duplicates of item ID.
"""
item_ids: Set[int] = {item_tables.get_full_item_list()[item_name].code for item_name in item_tables.get_full_item_list()}
self.assertEqual(len(item_ids), len(item_tables.get_full_item_list()))
def test_number_distinct_in_item_type(self) -> None:
"""
Tests if each item is distinct for sending into the mod.
"""
item_types: List[item_tables.ItemTypeEnum] = [
*[item.value for item in item_tables.TerranItemType],
*[item.value for item in item_tables.ZergItemType],
*[item.value for item in item_tables.ProtossItemType],
*[item.value for item in item_tables.FactionlessItemType]
]
self.assertGreater(len(item_types), 0)
for item_type in item_types:
item_names: List[str] = [
item_name for item_name in item_tables.get_full_item_list()
if item_tables.get_full_item_list()[item_name].number >= 0 # Negative numbers have special meaning
and item_tables.get_full_item_list()[item_name].type == item_type
]
item_numbers: Set[int] = {item_tables.get_full_item_list()[item_name] for item_name in item_names}
self.assertEqual(len(item_names), len(item_numbers))
def test_progressive_has_quantity(self) -> None:
"""
:return:
"""
progressive_groups: List[item_tables.ItemTypeEnum] = [
item_tables.TerranItemType.Progressive,
item_tables.TerranItemType.Progressive_2,
item_tables.ProtossItemType.Progressive,
item_tables.ZergItemType.Progressive
]
quantities: List[int] = [
item_tables.get_full_item_list()[item].quantity for item in item_tables.get_full_item_list()
if item_tables.get_full_item_list()[item].type in progressive_groups
]
self.assertNotIn(1, quantities)
def test_non_progressive_quantity(self) -> None:
"""
Check if non-progressive items have quantity at most 1.
"""
non_progressive_single_entity_groups: List[item_tables.ItemTypeEnum] = [
# Terran
item_tables.TerranItemType.Unit,
item_tables.TerranItemType.Unit_2,
item_tables.TerranItemType.Mercenary,
item_tables.TerranItemType.Armory_1,
item_tables.TerranItemType.Armory_2,
item_tables.TerranItemType.Armory_3,
item_tables.TerranItemType.Armory_4,
item_tables.TerranItemType.Armory_5,
item_tables.TerranItemType.Armory_6,
item_tables.TerranItemType.Armory_7,
item_tables.TerranItemType.Building,
item_tables.TerranItemType.Laboratory,
item_tables.TerranItemType.Nova_Gear,
# Zerg
item_tables.ZergItemType.Unit,
item_tables.ZergItemType.Mercenary,
item_tables.ZergItemType.Morph,
item_tables.ZergItemType.Strain,
item_tables.ZergItemType.Mutation_1,
item_tables.ZergItemType.Mutation_2,
item_tables.ZergItemType.Mutation_3,
item_tables.ZergItemType.Evolution_Pit,
item_tables.ZergItemType.Ability,
# Protoss
item_tables.ProtossItemType.Unit,
item_tables.ProtossItemType.Unit_2,
item_tables.ProtossItemType.Building,
item_tables.ProtossItemType.Forge_1,
item_tables.ProtossItemType.Forge_2,
item_tables.ProtossItemType.Forge_3,
item_tables.ProtossItemType.Forge_4,
item_tables.ProtossItemType.Solarite_Core,
item_tables.ProtossItemType.Spear_Of_Adun
]
quantities: List[int] = [
item_tables.get_full_item_list()[item].quantity for item in item_tables.get_full_item_list()
if item_tables.get_full_item_list()[item].type in non_progressive_single_entity_groups
]
for quantity in quantities:
self.assertLessEqual(quantity, 1)
def test_item_number_less_than_30(self) -> None:
"""
Checks if all item numbers are within bounds supported by game mod.
"""
not_checked_item_types: List[item_tables.ItemTypeEnum] = [
item_tables.ZergItemType.Level
]
items_to_check: List[str] = [
item for item in item_tables.get_full_item_list()
if item_tables.get_full_item_list()[item].type not in not_checked_item_types
]
for item in items_to_check:
item_number = item_tables.get_full_item_list()[item].number
self.assertLess(item_number, 30)

View File

@@ -0,0 +1,37 @@
import unittest
from .. import location_groups
from ..mission_tables import SC2Mission, MissionFlag
class TestLocationGroups(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.location_groups = location_groups.get_location_groups()
def test_location_categories_have_a_group(self) -> None:
self.assertIn('Victory', self.location_groups)
self.assertIn(f'{SC2Mission.LIBERATION_DAY.mission_name}: Victory', self.location_groups['Victory'])
self.assertIn(f'{SC2Mission.IN_UTTER_DARKNESS.mission_name}: Defeat', self.location_groups['Victory'])
self.assertIn('Vanilla', self.location_groups)
self.assertIn(f'{SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name}: Close Relic', self.location_groups['Vanilla'])
self.assertIn('Extra', self.location_groups)
self.assertIn(f'{SC2Mission.SMASH_AND_GRAB.mission_name}: First Forcefield Area Busted', self.location_groups['Extra'])
self.assertIn('Challenge', self.location_groups)
self.assertIn(f'{SC2Mission.ZERO_HOUR.mission_name}: First Hatchery', self.location_groups['Challenge'])
self.assertIn('Mastery', self.location_groups)
self.assertIn(f'{SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name}: Protoss Cleared', self.location_groups['Mastery'])
def test_missions_have_a_group(self) -> None:
self.assertIn(SC2Mission.LIBERATION_DAY.mission_name, self.location_groups)
self.assertIn(f'{SC2Mission.LIBERATION_DAY.mission_name}: Victory', self.location_groups[SC2Mission.LIBERATION_DAY.mission_name])
self.assertIn(f'{SC2Mission.LIBERATION_DAY.mission_name}: Special Delivery', self.location_groups[SC2Mission.LIBERATION_DAY.mission_name])
def test_race_swapped_locations_share_a_group(self) -> None:
self.assertIn(MissionFlag.HasRaceSwap, SC2Mission.ZERO_HOUR.flags)
ZERO_HOUR = 'Zero Hour'
self.assertNotEqual(ZERO_HOUR, SC2Mission.ZERO_HOUR.mission_name)
self.assertIn(ZERO_HOUR, self.location_groups)
self.assertIn(f'{ZERO_HOUR}: Victory', self.location_groups)
self.assertIn(f'{SC2Mission.ZERO_HOUR.mission_name}: Victory', self.location_groups[f'{ZERO_HOUR}: Victory'])
self.assertIn(f'{SC2Mission.ZERO_HOUR_P.mission_name}: Victory', self.location_groups[f'{ZERO_HOUR}: Victory'])
self.assertIn(f'{SC2Mission.ZERO_HOUR_Z.mission_name}: Victory', self.location_groups[f'{ZERO_HOUR}: Victory'])

View File

@@ -0,0 +1,9 @@
import unittest
from .. import mission_groups
class TestMissionGroups(unittest.TestCase):
def test_all_mission_groups_are_defined_and_nonempty(self) -> None:
for mission_group_name in mission_groups.MissionGroupNames.get_all_group_names():
self.assertIn(mission_group_name, mission_groups.mission_groups)
self.assertTrue(mission_groups.mission_groups[mission_group_name])

View File

@@ -1,7 +1,19 @@
import unittest
from .test_base import Sc2TestBase
from .. import Options, MissionTables
from typing import Dict
from .. import options
from ..item import item_parents
class TestOptions(unittest.TestCase):
def test_campaign_size_option_max_matches_number_of_missions(self):
self.assertEqual(Options.MaximumCampaignSize.range_end, len(MissionTables.SC2Mission))
def test_unit_max_upgrades_matching_items(self) -> None:
upgrade_group_to_count: Dict[str, int] = {}
for parent_id, child_list in item_parents.parent_id_to_children.items():
main_parent = item_parents.parent_present[parent_id].constraint_group
if main_parent is None:
continue
upgrade_group_to_count.setdefault(main_parent, 0)
upgrade_group_to_count[main_parent] += len(child_list)
self.assertEqual(options.MAX_UPGRADES_OPTION, max(upgrade_group_to_count.values()))

View File

@@ -0,0 +1,40 @@
import unittest
from .test_base import Sc2TestBase
from .. import mission_tables, SC2Campaign
from .. import options
from ..mission_order.layout_types import Grid
class TestGridsizes(unittest.TestCase):
def test_grid_sizes_meet_specs(self):
self.assertTupleEqual((1, 2, 0), Grid.get_grid_dimensions(2))
self.assertTupleEqual((1, 3, 0), Grid.get_grid_dimensions(3))
self.assertTupleEqual((2, 2, 0), Grid.get_grid_dimensions(4))
self.assertTupleEqual((2, 3, 1), Grid.get_grid_dimensions(5))
self.assertTupleEqual((2, 4, 1), Grid.get_grid_dimensions(7))
self.assertTupleEqual((2, 4, 0), Grid.get_grid_dimensions(8))
self.assertTupleEqual((3, 3, 0), Grid.get_grid_dimensions(9))
self.assertTupleEqual((2, 5, 0), Grid.get_grid_dimensions(10))
self.assertTupleEqual((3, 4, 1), Grid.get_grid_dimensions(11))
self.assertTupleEqual((3, 4, 0), Grid.get_grid_dimensions(12))
self.assertTupleEqual((3, 5, 0), Grid.get_grid_dimensions(15))
self.assertTupleEqual((4, 4, 0), Grid.get_grid_dimensions(16))
self.assertTupleEqual((4, 6, 0), Grid.get_grid_dimensions(24))
self.assertTupleEqual((5, 5, 0), Grid.get_grid_dimensions(25))
self.assertTupleEqual((5, 6, 1), Grid.get_grid_dimensions(29))
self.assertTupleEqual((5, 7, 2), Grid.get_grid_dimensions(33))
class TestGridGeneration(Sc2TestBase):
options = {
"mission_order": options.MissionOrder.option_grid,
"excluded_missions": [mission_tables.SC2Mission.ZERO_HOUR.mission_name,],
"enabled_campaigns": {
SC2Campaign.WOL.campaign_name,
SC2Campaign.PROPHECY.campaign_name,
}
}
def test_size_matches_exclusions(self):
self.assertNotIn(mission_tables.SC2Mission.ZERO_HOUR.mission_name, self.multiworld.regions)
# WoL has 29 missions. -1 for Zero Hour being excluded, +1 for the automatically-added menu location
self.assertEqual(len(self.multiworld.regions), 29)

View File

@@ -0,0 +1,186 @@
import itertools
from dataclasses import fields
from random import Random
import unittest
from typing import List, Set, Iterable
from BaseClasses import ItemClassification, MultiWorld
import Options as CoreOptions
from .. import options, locations
from ..item import item_tables
from ..rules import SC2Logic
from ..mission_tables import SC2Race, MissionFlag, lookup_name_to_mission
class TestInventory:
"""
Runs checks against inventory with validation if all target items are progression and returns a random result
"""
def __init__(self) -> None:
self.random: Random = Random()
self.progression_types: Set[ItemClassification] = {ItemClassification.progression, ItemClassification.progression_skip_balancing}
def is_item_progression(self, item: str) -> bool:
return item_tables.item_table[item].classification in self.progression_types
def random_boolean(self):
return self.random.choice([True, False])
def has(self, item: str, player: int, count: int = 1):
if not self.is_item_progression(item):
raise AssertionError("Logic item {} is not a progression item".format(item))
return self.random_boolean()
def has_any(self, items: Set[str], player: int):
non_progression_items = [item for item in items if not self.is_item_progression(item)]
if len(non_progression_items) > 0:
raise AssertionError("Logic items {} are not progression items".format(non_progression_items))
return self.random_boolean()
def has_all(self, items: Set[str], player: int):
return self.has_any(items, player)
def has_group(self, item_group: str, player: int, count: int = 1):
return self.random_boolean()
def count_group(self, item_name_group: str, player: int) -> int:
return self.random.randrange(0, 20)
def count(self, item: str, player: int) -> int:
if not self.is_item_progression(item):
raise AssertionError("Item {} is not a progression item".format(item))
random_value: int = self.random.randrange(0, 5)
if random_value == 4: # 0-3 has a higher chance due to logic rules
return self.random.randrange(4, 100)
else:
return random_value
def count_from_list(self, items: Iterable[str], player: int) -> int:
return sum(self.count(item_name, player) for item_name in items)
def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
return sum(self.count(item_name, player) for item_name in items)
class TestWorld:
"""
Mock world to simulate different player options for logic rules
"""
def __init__(self) -> None:
defaults = dict()
for field in fields(options.Starcraft2Options):
field_class = field.type
option_name = field.name
if isinstance(field_class, str):
if field_class in globals():
field_class = globals()[field_class]
else:
field_class = CoreOptions.__dict__[field.type]
defaults[option_name] = field_class(options.get_option_value(None, option_name))
self.options: options.Starcraft2Options = options.Starcraft2Options(**defaults)
self.options.mission_order.value = options.MissionOrder.option_vanilla_shuffled
self.player = 1
self.multiworld = MultiWorld(1)
class TestRules(unittest.TestCase):
def setUp(self) -> None:
self.required_tactics_values: List[int] = [
options.RequiredTactics.option_standard, options.RequiredTactics.option_advanced
]
self.all_in_map_values: List[int] = [
options.AllInMap.option_ground, options.AllInMap.option_air
]
self.take_over_ai_allies_values: List[int] = [
options.TakeOverAIAllies.option_true, options.TakeOverAIAllies.option_false
]
self.kerrigan_presence_values: List[int] = [
options.KerriganPresence.option_vanilla, options.KerriganPresence.option_not_present
]
self.NUM_TEST_RUNS = 100
@staticmethod
def _get_world(
required_tactics: int = options.RequiredTactics.default,
all_in_map: int = options.AllInMap.default,
take_over_ai_allies: int = options.TakeOverAIAllies.default,
kerrigan_presence: int = options.KerriganPresence.default,
# setting this to everywhere catches one extra logic check for Amon's Fall without missing any
spear_of_adun_passive_presence: int = options.SpearOfAdunPassiveAbilityPresence.option_everywhere,
) -> TestWorld:
test_world = TestWorld()
test_world.options.required_tactics.value = required_tactics
test_world.options.all_in_map.value = all_in_map
test_world.options.take_over_ai_allies.value = take_over_ai_allies
test_world.options.kerrigan_presence.value = kerrigan_presence
test_world.options.spear_of_adun_passive_ability_presence.value = spear_of_adun_passive_presence
test_world.logic = SC2Logic(test_world) # type: ignore
return test_world
def test_items_in_rules_are_progression(self):
test_inventory = TestInventory()
for option in self.required_tactics_values:
test_world = self._get_world(required_tactics=option)
location_data = locations.get_locations(test_world)
for location in location_data:
for _ in range(self.NUM_TEST_RUNS):
location.rule(test_inventory)
def test_items_in_all_in_are_progression(self):
test_inventory = TestInventory()
for test_options in itertools.product(self.required_tactics_values, self.all_in_map_values):
test_world = self._get_world(required_tactics=test_options[0], all_in_map=test_options[1])
for location in locations.get_locations(test_world):
if 'All-In' not in location.region:
continue
for _ in range(self.NUM_TEST_RUNS):
location.rule(test_inventory)
def test_items_in_kerriganless_missions_are_progression(self):
test_inventory = TestInventory()
for test_options in itertools.product(self.required_tactics_values, self.kerrigan_presence_values):
test_world = self._get_world(required_tactics=test_options[0], kerrigan_presence=test_options[1])
for location in locations.get_locations(test_world):
mission = lookup_name_to_mission[location.region]
if MissionFlag.Kerrigan not in mission.flags:
continue
for _ in range(self.NUM_TEST_RUNS):
location.rule(test_inventory)
def test_items_in_ai_takeover_missions_are_progression(self):
test_inventory = TestInventory()
for test_options in itertools.product(self.required_tactics_values, self.take_over_ai_allies_values):
test_world = self._get_world(required_tactics=test_options[0], take_over_ai_allies=test_options[1])
for location in locations.get_locations(test_world):
mission = lookup_name_to_mission[location.region]
if MissionFlag.AiAlly not in mission.flags:
continue
for _ in range(self.NUM_TEST_RUNS):
location.rule(test_inventory)
def test_items_in_hard_rules_are_progression(self):
test_inventory = TestInventory()
test_world = TestWorld()
test_world.options.required_tactics.value = options.RequiredTactics.option_any_units
test_world.logic = SC2Logic(test_world)
location_data = locations.get_locations(test_world)
for location in location_data:
if location.hard_rule is not None:
for _ in range(10):
location.hard_rule(test_inventory)
def test_items_in_any_units_rules_are_progression(self):
test_inventory = TestInventory()
test_world = TestWorld()
test_world.options.required_tactics.value = options.RequiredTactics.option_any_units
logic = SC2Logic(test_world)
test_world.logic = logic
for race in (SC2Race.TERRAN, SC2Race.PROTOSS, SC2Race.ZERG):
for target in range(1, 5):
rule = logic.has_race_units(target, race)
for _ in range(10):
rule(test_inventory)

View File

@@ -0,0 +1,492 @@
"""
Unit tests for yaml usecases we want to support
"""
from .test_base import Sc2SetupTestBase
from .. import get_all_missions, mission_tables, options
from ..item import item_groups, item_tables, item_names
from ..mission_tables import SC2Race, SC2Mission, SC2Campaign, MissionFlag
from ..options import EnabledCampaigns, MasteryLocations
class TestSupportedUseCases(Sc2SetupTestBase):
def test_vanilla_all_campaigns_generates(self) -> None:
world_options = {
'mission_order': options.MissionOrder.option_vanilla,
'enabled_campaigns': EnabledCampaigns.valid_keys,
}
self.generate_world(world_options)
world_regions = [region.name for region in self.multiworld.regions if region.name != "Menu"]
self.assertEqual(len(world_regions), 83, "Unexpected number of missions for vanilla mission order")
def test_terran_with_nco_units_only_generates(self):
world_options = {
'enabled_campaigns': {
SC2Campaign.WOL.campaign_name,
SC2Campaign.NCO.campaign_name
},
'excluded_items': {
item_groups.ItemGroupNames.TERRAN_UNITS: 0,
},
'unexcluded_items': {
item_groups.ItemGroupNames.NCO_UNITS: 0,
},
'max_number_of_upgrades': 2,
}
self.generate_world(world_options)
self.assertTrue(self.multiworld.itempool)
world_item_names = [item.name for item in self.multiworld.itempool]
self.assertIn(item_names.MARINE, world_item_names)
self.assertIn(item_names.RAVEN, world_item_names)
self.assertIn(item_names.LIBERATOR, world_item_names)
self.assertIn(item_names.BATTLECRUISER, world_item_names)
self.assertNotIn(item_names.DIAMONDBACK, world_item_names)
self.assertNotIn(item_names.DIAMONDBACK_BURST_CAPACITORS, world_item_names)
self.assertNotIn(item_names.VIKING, world_item_names)
def test_nco_with_nobuilds_excluded_generates(self):
world_options = {
'enabled_campaigns': {
SC2Campaign.NCO.campaign_name
},
'shuffle_no_build': options.ShuffleNoBuild.option_false,
'mission_order': options.MissionOrder.option_mini_campaign,
}
self.generate_world(world_options)
self.assertTrue(self.multiworld.itempool)
missions = get_all_missions(self.world.custom_mission_order)
self.assertNotIn(mission_tables.SC2Mission.THE_ESCAPE, missions)
self.assertNotIn(mission_tables.SC2Mission.IN_THE_ENEMY_S_SHADOW, missions)
for mission in missions:
self.assertEqual(mission_tables.SC2Campaign.NCO, mission.campaign)
def test_terran_with_nco_upgrades_units_only_generates(self):
world_options = {
'enabled_campaigns': {
SC2Campaign.WOL.campaign_name,
SC2Campaign.NCO.campaign_name
},
'mission_order': options.MissionOrder.option_vanilla_shuffled,
'excluded_items': {
item_groups.ItemGroupNames.TERRAN_ITEMS: 0,
},
'unexcluded_items': {
item_groups.ItemGroupNames.NCO_MAX_PROGRESSIVE_ITEMS: 0,
item_groups.ItemGroupNames.NCO_MIN_PROGRESSIVE_ITEMS: 1,
},
'excluded_missions': [
# These missions have trouble fulfilling Terran Power Rating under these terms
SC2Mission.SUPERNOVA.mission_name,
SC2Mission.WELCOME_TO_THE_JUNGLE.mission_name,
SC2Mission.TROUBLE_IN_PARADISE.mission_name,
],
'mastery_locations': MasteryLocations.option_disabled,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool + self.multiworld.precollected_items[1]]
self.assertTrue(world_item_names)
missions = get_all_missions(self.world.custom_mission_order)
for mission in missions:
self.assertIn(mission_tables.MissionFlag.Terran, mission.flags)
self.assertIn(item_names.MARINE, world_item_names)
self.assertIn(item_names.MARAUDER, world_item_names)
self.assertIn(item_names.BUNKER, world_item_names)
self.assertIn(item_names.BANSHEE, world_item_names)
self.assertIn(item_names.BATTLECRUISER_ATX_LASER_BATTERY, world_item_names)
self.assertIn(item_names.NOVA_C20A_CANISTER_RIFLE, world_item_names)
self.assertGreaterEqual(world_item_names.count(item_names.BANSHEE_PROGRESSIVE_CROSS_SPECTRUM_DAMPENERS), 2)
self.assertGreaterEqual(world_item_names.count(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON), 3)
self.assertNotIn(item_names.MEDIC, world_item_names)
self.assertNotIn(item_names.PSI_DISRUPTER, world_item_names)
self.assertNotIn(item_names.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS, world_item_names)
self.assertNotIn(item_names.HELLION_INFERNAL_PLATING, world_item_names)
self.assertNotIn(item_names.CELLULAR_REACTOR, world_item_names)
self.assertNotIn(item_names.TECH_REACTOR, world_item_names)
def test_nco_and_2_wol_missions_only_can_generate_with_vanilla_items_only(self) -> None:
world_options = {
'enabled_campaigns': {
SC2Campaign.WOL.campaign_name,
SC2Campaign.NCO.campaign_name
},
'excluded_missions': [
mission.mission_name for mission in mission_tables.SC2Mission
if mission.campaign == mission_tables.SC2Campaign.WOL
and mission.mission_name not in (mission_tables.SC2Mission.LIBERATION_DAY.mission_name, mission_tables.SC2Mission.THE_OUTLAWS.mission_name)
],
'mission_order': options.MissionOrder.option_grid,
'maximum_campaign_size': options.MaximumCampaignSize.range_end,
'mastery_locations': options.MasteryLocations.option_disabled,
'vanilla_items_only': True,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
self.assertTrue(item_names)
self.assertNotIn(item_names.LIBERATOR, world_item_names)
self.assertNotIn(item_names.MARAUDER_PROGRESSIVE_STIMPACK, world_item_names)
self.assertNotIn(item_names.HELLION_HELLBAT, world_item_names)
self.assertNotIn(item_names.BATTLECRUISER_CLOAK, world_item_names)
def test_free_protoss_only_generates(self) -> None:
world_options = {
'enabled_campaigns': {
SC2Campaign.PROPHECY.campaign_name,
SC2Campaign.PROLOGUE.campaign_name
},
# todo(mm): Currently, these settings don't generate on grid because there are not enough EASY missions
'mission_order': options.MissionOrder.option_vanilla_shuffled,
'maximum_campaign_size': options.MaximumCampaignSize.range_end,
'accessibility': 'locations',
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
self.assertTrue(world_item_names)
missions = get_all_missions(self.world.custom_mission_order)
self.assertEqual(len(missions), 7, "Wrong number of missions in free protoss seed")
for mission in missions:
self.assertIn(mission.campaign, (mission_tables.SC2Campaign.PROLOGUE, mission_tables.SC2Campaign.PROPHECY))
for item_name in world_item_names:
self.assertIn(item_tables.item_table[item_name].race, (mission_tables.SC2Race.ANY, mission_tables.SC2Race.PROTOSS))
def test_resource_filler_items_may_be_put_in_start_inventory(self) -> None:
NUM_RESOURCE_ITEMS = 10
world_options = {
'start_inventory': {
item_names.STARTING_MINERALS: NUM_RESOURCE_ITEMS,
item_names.STARTING_VESPENE: NUM_RESOURCE_ITEMS,
item_names.STARTING_SUPPLY: NUM_RESOURCE_ITEMS,
},
}
self.generate_world(world_options)
start_item_names = [item.name for item in self.multiworld.precollected_items[self.player]]
self.assertEqual(start_item_names.count(item_names.STARTING_MINERALS), NUM_RESOURCE_ITEMS, "Wrong number of starting minerals in starting inventory")
self.assertEqual(start_item_names.count(item_names.STARTING_VESPENE), NUM_RESOURCE_ITEMS, "Wrong number of starting vespene in starting inventory")
self.assertEqual(start_item_names.count(item_names.STARTING_SUPPLY), NUM_RESOURCE_ITEMS, "Wrong number of starting supply in starting inventory")
def test_excluding_protoss_excludes_campaigns_and_items(self) -> None:
world_options = {
'selected_races': {
SC2Race.TERRAN.get_title(),
SC2Race.ZERG.get_title(),
},
'enabled_campaigns': options.EnabledCampaigns.valid_keys,
'mission_order': options.MissionOrder.option_grid,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
world_regions = [region.name for region in self.multiworld.regions]
world_regions.remove('Menu')
for item_name in world_item_names:
self.assertNotEqual(item_tables.item_table[item_name].race, mission_tables.SC2Race.PROTOSS, f"{item_name} is a PROTOSS item!")
for region in world_regions:
self.assertNotIn(mission_tables.lookup_name_to_mission[region].campaign,
(mission_tables.SC2Campaign.LOTV, mission_tables.SC2Campaign.PROPHECY, mission_tables.SC2Campaign.PROLOGUE),
f"{region} is a PROTOSS mission!")
def test_excluding_terran_excludes_campaigns_and_items(self) -> None:
world_options = {
'selected_races': {
SC2Race.ZERG.get_title(),
SC2Race.PROTOSS.get_title(),
},
'enabled_campaigns': EnabledCampaigns.valid_keys,
'mission_order': options.MissionOrder.option_grid,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
world_regions = [region.name for region in self.multiworld.regions]
world_regions.remove('Menu')
for item_name in world_item_names:
self.assertNotEqual(item_tables.item_table[item_name].race, mission_tables.SC2Race.TERRAN,
f"{item_name} is a TERRAN item!")
for region in world_regions:
self.assertNotIn(mission_tables.lookup_name_to_mission[region].campaign,
(mission_tables.SC2Campaign.WOL, mission_tables.SC2Campaign.NCO),
f"{region} is a TERRAN mission!")
def test_excluding_zerg_excludes_campaigns_and_items(self) -> None:
world_options = {
'selected_races': {
SC2Race.TERRAN.get_title(),
SC2Race.PROTOSS.get_title(),
},
'enabled_campaigns': EnabledCampaigns.valid_keys,
'mission_order': options.MissionOrder.option_grid,
'excluded_missions': [
SC2Mission.THE_INFINITE_CYCLE.mission_name
]
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
world_regions = [region.name for region in self.multiworld.regions]
world_regions.remove('Menu')
for item_name in world_item_names:
self.assertNotEqual(item_tables.item_table[item_name].race, mission_tables.SC2Race.ZERG,
f"{item_name} is a ZERG item!")
# have to manually exclude the only non-zerg HotS mission...
for region in filter(lambda region: region != "With Friends Like These", world_regions):
self.assertNotIn(mission_tables.lookup_name_to_mission[region].campaign,
([mission_tables.SC2Campaign.HOTS]),
f"{region} is a ZERG mission!")
def test_excluding_faction_on_vanilla_order_excludes_epilogue(self) -> None:
world_options = {
'selected_races': {
SC2Race.TERRAN.get_title(),
SC2Race.PROTOSS.get_title(),
},
'enabled_campaigns': EnabledCampaigns.valid_keys,
'mission_order': options.MissionOrder.option_vanilla,
}
self.generate_world(world_options)
world_regions = [region.name for region in self.multiworld.regions]
world_regions.remove('Menu')
for region in world_regions:
self.assertNotIn(mission_tables.lookup_name_to_mission[region].campaign,
([mission_tables.SC2Campaign.EPILOGUE]),
f"{region} is an epilogue mission!")
def test_race_swap_pick_one_has_correct_length_and_includes_swaps(self) -> None:
world_options = {
'selected_races': options.SelectRaces.valid_keys,
'enable_race_swap': options.EnableRaceSwapVariants.option_pick_one,
'enabled_campaigns': {
SC2Campaign.WOL.campaign_name,
},
'mission_order': options.MissionOrder.option_grid,
'excluded_missions': [mission_tables.SC2Mission.ZERO_HOUR.mission_name],
}
self.generate_world(world_options)
world_regions = [region.name for region in self.multiworld.regions]
world_regions.remove('Menu')
NUM_WOL_MISSIONS = len([mission for mission in SC2Mission if mission.campaign == SC2Campaign.WOL and MissionFlag.RaceSwap not in mission.flags])
races = set(mission_tables.lookup_name_to_mission[mission].race for mission in world_regions)
self.assertEqual(len(world_regions), NUM_WOL_MISSIONS)
self.assertTrue(SC2Race.ZERG in races or SC2Race.PROTOSS in races)
def test_start_inventory_upgrade_level_includes_only_correct_bundle(self) -> None:
world_options = {
'start_inventory': {
item_groups.ItemGroupNames.TERRAN_GENERIC_UPGRADES: 1,
},
'locked_items': {
# One unit of each class to guarantee upgrades are available
item_names.MARINE: 1,
item_names.VULTURE: 1,
item_names.BANSHEE: 1,
},
'generic_upgrade_items': options.GenericUpgradeItems.option_bundle_unit_class,
'selected_races': {
SC2Race.TERRAN.get_title(),
},
'enable_race_swap': options.EnableRaceSwapVariants.option_disabled,
'enabled_campaigns': {
SC2Campaign.WOL.campaign_name,
},
'mission_order': options.MissionOrder.option_grid,
}
self.generate_world(world_options)
self.assertTrue(self.multiworld.itempool)
world_item_names = [item.name for item in self.multiworld.itempool]
start_inventory = [item.name for item in self.multiworld.precollected_items[self.player]]
# Start inventory
self.assertIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE, start_inventory)
self.assertIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE, start_inventory)
self.assertIn(item_names.PROGRESSIVE_TERRAN_SHIP_UPGRADE, start_inventory)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, start_inventory)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, start_inventory)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON, start_inventory)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR, start_inventory)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, start_inventory)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR, start_inventory)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_ARMOR_UPGRADE, start_inventory)
# Additional items in pool -- standard tactics will require additional levels
self.assertIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_UPGRADE, world_item_names)
self.assertIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_UPGRADE, world_item_names)
self.assertIn(item_names.PROGRESSIVE_TERRAN_SHIP_UPGRADE, world_item_names)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_WEAPON, world_item_names)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_INFANTRY_ARMOR, world_item_names)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_WEAPON, world_item_names)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_VEHICLE_ARMOR, world_item_names)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_SHIP_WEAPON, world_item_names)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_SHIP_ARMOR, world_item_names)
self.assertNotIn(item_names.PROGRESSIVE_TERRAN_ARMOR_UPGRADE, world_item_names)
def test_kerrigan_max_active_abilities(self):
target_number: int = 8
world_options = {
'mission_order': options.MissionOrder.option_grid,
'maximum_campaign_size': options.MaximumCampaignSize.range_end,
'selected_races': {
SC2Race.ZERG.get_title(),
},
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
'kerrigan_max_active_abilities': target_number,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
kerrigan_actives = [item_name for item_name in world_item_names if item_name in item_groups.kerrigan_active_abilities]
self.assertLessEqual(len(kerrigan_actives), target_number)
def test_kerrigan_max_passive_abilities(self):
target_number: int = 3
world_options = {
'mission_order': options.MissionOrder.option_grid,
'maximum_campaign_size': options.MaximumCampaignSize.range_end,
'selected_races': {
SC2Race.ZERG.get_title(),
},
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
'kerrigan_max_passive_abilities': target_number,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
kerrigan_passives = [item_name for item_name in world_item_names if item_name in item_groups.kerrigan_passives]
self.assertLessEqual(len(kerrigan_passives), target_number)
def test_spear_of_adun_max_active_abilities(self):
target_number: int = 8
world_options = {
'mission_order': options.MissionOrder.option_grid,
'maximum_campaign_size': options.MaximumCampaignSize.range_end,
'selected_races': {
SC2Race.PROTOSS.get_title(),
},
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
'spear_of_adun_max_active_abilities': target_number,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
spear_of_adun_actives = [item_name for item_name in world_item_names if item_name in item_tables.spear_of_adun_calldowns]
self.assertLessEqual(len(spear_of_adun_actives), target_number)
def test_spear_of_adun_max_autocasts(self):
target_number: int = 2
world_options = {
'mission_order': options.MissionOrder.option_grid,
'maximum_campaign_size': options.MaximumCampaignSize.range_end,
'selected_races': {
SC2Race.PROTOSS.get_title(),
},
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
'spear_of_adun_max_passive_abilities': target_number,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
spear_of_adun_autocasts = [item_name for item_name in world_item_names if item_name in item_tables.spear_of_adun_castable_passives]
self.assertLessEqual(len(spear_of_adun_autocasts), target_number)
def test_nova_max_weapons(self):
target_number: int = 3
world_options = {
'mission_order': options.MissionOrder.option_grid,
'maximum_campaign_size': options.MaximumCampaignSize.range_end,
'selected_races': {
SC2Race.TERRAN.get_title(),
},
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
'nova_max_weapons': target_number,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
nova_weapons = [item_name for item_name in world_item_names if item_name in item_groups.nova_weapons]
self.assertLessEqual(len(nova_weapons), target_number)
def test_nova_max_gadgets(self):
target_number: int = 3
world_options = {
'mission_order': options.MissionOrder.option_grid,
'maximum_campaign_size': options.MaximumCampaignSize.range_end,
'selected_races': {
SC2Race.TERRAN.get_title(),
},
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
'nova_max_gadgets': target_number,
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
nova_gadgets = [item_name for item_name in world_item_names if item_name in item_groups.nova_gadgets]
self.assertLessEqual(len(nova_gadgets), target_number)
def test_mercs_only(self) -> None:
world_options = {
'selected_races': [
SC2Race.TERRAN.get_title(),
SC2Race.ZERG.get_title(),
],
'required_tactics': options.RequiredTactics.option_any_units,
'excluded_items': {
item_groups.ItemGroupNames.TERRAN_UNITS: 0,
item_groups.ItemGroupNames.ZERG_UNITS: 0,
},
'unexcluded_items': {
item_groups.ItemGroupNames.TERRAN_MERCENARIES: 0,
item_groups.ItemGroupNames.ZERG_MERCENARIES: 0,
},
'start_inventory': {
item_names.PROGRESSIVE_FAST_DELIVERY: 1,
item_names.ROGUE_FORCES: 1,
item_names.UNRESTRICTED_MUTATION: 1,
item_names.EVOLUTIONARY_LEAP: 1,
},
'mission_order': options.MissionOrder.option_grid,
'excluded_missions': [
SC2Mission.ENEMY_WITHIN.mission_name, # Requires a unit for Niadra to build
],
}
self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool]
terran_nonmerc_units = tuple(
item_name
for item_name in world_item_names
if item_name in item_groups.terran_units and item_name not in item_groups.terran_mercenaries
)
zerg_nonmerc_units = tuple(
item_name
for item_name in world_item_names
if item_name in item_groups.zerg_units and item_name not in item_groups.zerg_mercenaries
)
self.assertTupleEqual(terran_nonmerc_units, ())
self.assertTupleEqual(zerg_nonmerc_units, ())