Files
dockipelago/worlds/mkdd/__init__.py
Jonathan Tinney 7971961166
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

333 lines
16 KiB
Python

"""
Archipelago init file for Mario Kart Double Dash!!
"""
import logging
import math
from typing import Any
from BaseClasses import Region, ItemClassification, Tutorial
from worlds.AutoWorld import WebWorld, World
from worlds.LauncherComponents import Component, components, launch_subprocess
from . import game_data, locations, items, regions, version
from .items import MkddItem
from .locations import MkddLocation, MkddLocationData
from .options import MkddOptions
from .regions import MkddRegionData
from .rules import MkddRules
from .settings import MkddSettings
class MkddWebWorld(WebWorld):
theme = "ocean"
tutorials = [
Tutorial(
tutorial_name="Setup Guide",
description="A guide to setting up Mario Kart Double Dash for Archipelago.",
language="English",
file_name="en_Setup.md",
link="setup/en",
authors=["aXu"],
)
]
class MkddWorld(World):
"""
The fourth entry in Mario Kart series, Double Dash shakes up the gameplay by introducing 2 drivers per vehicle.
"""
game = version.get_game_name()
web = MkddWebWorld()
options_dataclass = MkddOptions
options: MkddOptions
settings: MkddSettings
item_name_to_id = items.name_to_id
item_name_groups = items.groups
location_name_to_id = locations.name_to_id
location_name_groups = locations.groups
# Universal Tracker.
ut_can_gen_without_yaml = True
glitches_item_name = items.SKIP_DIFFICULTY
def __init__(self, world, player):
self.current_locations: list[MkddLocationData] = []
self.current_regions: dict[str, MkddRegionData] = {}
self.current_entrances: set[str] = set()
self.cups_courses: list[list[int]] = []
self.character_item_total_weights: dict[str, list[int]] = {}
self.global_items_total_weights: list[int] = []
super(MkddWorld, self).__init__(world, player)
def generate_early(self):
# Adjust amount of trophies in the pool if the requirement is too high.
max_requirement: int = self.options.shuffle_extra_trophies.value
if self.options.grand_prix_trophies:
max_requirement += 16
if self.options.trophy_requirement.value > max_requirement:
logging.getLogger("MKDD Logger").warning(f"{self.player_name}: Requirement for trophies is higher than available trophies. Adding extra trophies...")
self.options.shuffle_extra_trophies.value = self.options.trophy_requirement.value
if self.options.grand_prix_trophies:
self.options.shuffle_extra_trophies.value -= 16
# Universal Tracker passthrough.
if hasattr(self.multiworld, "re_gen_passthrough"):
slot_data: dict = self.multiworld.re_gen_passthrough["Mario Kart Double Dash"]
self.options.trophy_requirement = slot_data["trophy_requirement"]
self.options.logic_difficulty = slot_data["logic_difficulty"]
# Staff ghosts were on by default before this option was introduced.
self.options.time_trials = slot_data.get("time_trials", options.TimeTrials.option_include_staff_ghosts)
self.options.item_boxes_as_locations = slot_data["item_boxes_as_locations"]
def create_regions(self) -> None:
# Course shuffle (entrance rando). If using Universal Tracker, get shuffled tracks from slot data.
# Course order is kept in a list[list[int]], where first index is cup, and second index points to a course inside that cup.
if hasattr(self.multiworld, "re_gen_passthrough"):
slot_data = self.multiworld.re_gen_passthrough["Mario Kart Double Dash"]
self.cups_courses = slot_data["cups_courses"]
else:
all_courses: list[int] = list(range(16))
if self.options.course_shuffle == options.CourseShuffle.option_shuffle_once:
self.random.shuffle(all_courses)
self.cups_courses: list[list[int]] = []
for i in range(0, 16, 4):
self.cups_courses.append([
all_courses[i],
all_courses[i + 1],
all_courses[i + 2],
all_courses[i + 3],
])
# Create regions.
for region_name, region_data in regions.data_table.items():
if self.options.goal == options.Goal.option_trophies and region_name == game_data.CUPS[game_data.CUP_ALL_CUP_TOUR]:
continue
if self.options.time_trials == options.TimeTrials.option_disable and regions.TAG_TIME_TRIALS in region_data.tags:
continue
region = Region(region_name, self.player, self.multiworld)
self.multiworld.regions.append(region)
self.current_regions[region_name] = region_data
for region_name, region_data in self.current_regions.items():
# Connect regions.
region = self.get_region(region_name)
region.add_exits([exit for exit in region_data.connecting_regions if exit in self.current_regions.keys()])
if region_name in game_data.NORMAL_CUPS:
cup_no = game_data.CUPS.index(region_name)
region.add_exits([game_data.RACE_COURSES[self.cups_courses[cup_no][i]].name + " GP" for i in range(4)])
self.current_entrances.update([e.name for e in region.exits])
# Create locations.
for id, location_data in enumerate(locations.data_table):
if self.options.time_trials != options.TimeTrials.option_include_staff_ghosts and locations.TAG_TT_GHOST in location_data.tags:
continue
if not self.options.grand_prix_trophies and locations.TAG_CUP_TROPHY in location_data.tags:
continue
if self.options.item_boxes_as_locations == 0 and locations.TAG_ITEM_BOX in location_data.tags:
continue
if id > 0 and location_data.region == region_name:
region.add_locations({location_data.name: id})
self.current_locations.append(location_data)
# Locked items.
if self.options.grand_prix_trophies:
for cup in game_data.NORMAL_CUPS:
for vehicle_class in range(4):
self.get_location(locations.get_loc_name_trophy(cup, vehicle_class))\
.place_locked_item(self.create_item(items.TROPHY))
if self.options.goal == options.Goal.option_all_cup_tour:
self.get_location(locations.TROPHY_GOAL).place_locked_item(self.create_item(game_data.CUPS[game_data.CUP_ALL_CUP_TOUR]))
self.get_location(locations.WIN_ALL_CUP_TOUR).place_locked_item(self.create_item("Victory"))
elif self.options.goal == options.Goal.option_trophies:
self.get_location(locations.TROPHY_GOAL).place_locked_item(self.create_item("Victory"))
def create_items(self) -> None:
total_locations = len(self.multiworld.get_unfilled_locations(self.player))
# (item_name, count)
precollected: list[str] = []
# Give 1 cup, can't be All Star Cup.
precollected.append(self.random.choice(game_data.NORMAL_CUPS))
# Give 1 time trial track.
if self.options.time_trials != options.TimeTrials.option_disable:
precollected.append(items.get_item_name_tt_course(self.random.choice(game_data.RACE_COURSES).name))
# Give 2 random characters to begin.
precollected_characters = 0
while precollected_characters < 2:
character_name: str = self.random.choice(game_data.CHARACTERS).name
if not character_name in precollected:
precollected.append(character_name)
precollected_characters += 1
# Give 1 kart in each weight class.
for weight in range(3):
karts: list[str] = [kart.name for kart in game_data.KARTS if kart.weight == weight]
precollected.append(self.random.choice(karts))
if not self.options.speed_upgrades:
precollected.append(items.PROGRESSIVE_ENGINE)
# Set minimum difficulty on "hard", otherwise the seed can be unbeatable.
if self.options.logic_difficulty.value < game_data.ENGINE_UPGRADE_USEFULNESS:
self.options.logic_difficulty.value = game_data.ENGINE_UPGRADE_USEFULNESS
logging.getLogger("MKDD Logger").warning(f"{self.player_name}: No engine upgrades are available, setting difficulty to hard.")
for item in precollected:
self.multiworld.push_precollected(self.create_item(item))
# Generic items by predetermined counts.
item_pool: list[MkddItem] = []
for item in items.data_table:
if self.options.time_trials == options.TimeTrials.option_disable and (item.item_type == items.ItemType.TT_COURSE or item.name == items.PROGRESSIVE_TIME_TRIAL_ITEM):
continue
if item.classification != ItemClassification.filler:
count = item.count
count -= precollected.count(item.name)
for i in range(count):
item_pool.append(self.create_item(item.name))
for i in range(self.options.shuffle_extra_trophies):
item_pool.append(self.create_item(items.TROPHY))
if self.options.speed_upgrades:
for i in range(3):
item_pool.append(self.create_item(items.PROGRESSIVE_ENGINE))
# Kart upgrades generation.
if self.options.kart_upgrades > 0:
kart_weights = [5 for _ in game_data.KARTS]
upgrade_weights = [math.ceil(self.options.kart_upgrades / len(game_data.KART_UPGRADES)) for _ in game_data.KART_UPGRADES]
up_karts = self.random.sample(game_data.KARTS, self.options.kart_upgrades, counts = kart_weights)
upgrades = self.random.sample(game_data.KART_UPGRADES, self.options.kart_upgrades, counts = upgrade_weights)
for i in range(self.options.kart_upgrades):
item_pool.append(self.create_item(items.get_item_name_kart_upgrade(upgrades[i].name, up_karts[i].name)))
# Item box item generation.
# Give mostly bad items as global items.
items_left: list[game_data.Item] = [item for item in game_data.ITEMS if item != game_data.ITEM_NONE]
weights: list[str] = [1000 - item.usefulness ** 3 for item in game_data.ITEMS if item != game_data.ITEM_NONE]
global_items: list[game_data.Item] = []
for i in range(self.options.items_for_everybody):
item = self.random.sample(items_left, 1, counts = weights)[0]
item_pool.append(self.create_item(items.get_item_name_character_item(None, item.name)))
global_items.append(item)
id = items_left.index(item)
items_left.pop(id)
weights.pop(id)
# If there's too much global items there's going to be multiples.
# Make the pool bigger to avoid every character having the same items.
if len(items_left) < self.options.start_items_per_character + self.options.items_per_character:
items_left = [item for item in game_data.ITEMS if item != game_data.ITEM_NONE]
# Character specific items.
# Use items that aren't used as global items.
items_left_characters_pool: list[game_data.Item] = items_left.copy()
weights = [1 for _ in items_left]
items_per_character: dict[game_data.Character, list[game_data.Item]] = {character:[] for character in game_data.CHARACTERS}
for i in range(self.options.start_items_per_character + self.options.items_per_character):
for character in game_data.CHARACTERS:
# Try rolling for unique items.
for j in range(50):
item = self.random.sample(items_left, 1, counts = weights)[0]
if not item in items_per_character[character]:
break
# If item hasn't been found after 10 tries, try refilling the pool.
elif j == 10:
items_left = items_left_characters_pool.copy()
weights = [10 - item.usefulness for item in items_left]
items_per_character[character].append(item)
if i < self.options.start_items_per_character:
self.multiworld.push_precollected(self.create_item(items.get_item_name_character_item(character.name, item.name)))
else:
item_pool.append(self.create_item(items.get_item_name_character_item(character.name, item.name)))
id = items_left.index(item)
weights[id] -= 1
if weights[id] == 0:
items_left.pop(id)
weights.pop(id)
if len(items_left) == 0:
# Refill the pool with some balancing.
items_left = items_left_characters_pool.copy()
weights = [10 - item.usefulness for item in items_left]
# There can be too much of these, so generate only as long as there's enough locations.
if len(item_pool) == total_locations:
break
if len(item_pool) == total_locations:
break
self.character_item_total_weights = {character.name:[] for character in game_data.CHARACTERS}
for i in range(8):
self.global_items_total_weights.append(sum([item.weight_table[i] for item in global_items]))
for character in game_data.CHARACTERS:
self.character_item_total_weights[character.name].append(
sum([item.weight_table[i] for item in items_per_character[character]])
)
item_pool += [self.create_item(self.get_filler_item_name()) for _ in range(total_locations - len(item_pool))]
self.multiworld.itempool += item_pool
def create_item(self, name: str) -> MkddItem:
id = items.name_to_id[name]
item_data = items.data_table[id]
return MkddItem(name, item_data.classification, id, self.player)
def get_filler_item_name(self) -> str:
return items.RANDOM_ITEM
def set_rules(self) -> None:
rules = MkddRules(self)
rules.set_rules()
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
def collect(self, state, item) -> bool:
change = super().collect(state, item)
if change:
rules.add_item(state, self.player, item)
return change
def remove(self, state, item) -> bool:
change = super().remove(state, item)
if change:
rules.add_item(state, self.player, item, -1)
return change
def fill_slot_data(self) -> dict[str, Any]:
lap_counts = {course.name:course.laps for course in game_data.RACE_COURSES}
if self.options.shorter_courses:
for course, laps in lap_counts.items():
lap_counts[course] = int(math.ceil(laps * 2 / 3))
for course, laps in self.options.custom_lap_counts.items():
if laps > 0:
lap_counts[course] = laps
return {
"version": version.get_version(),
"trophy_requirement": int(self.options.trophy_requirement),
"logic_difficulty": int(self.options.logic_difficulty) if not self.options.tracker_unrestricted_logic else 100,
"time_trials": int(self.options.time_trials),
"cups_courses": self.cups_courses,
"all_cup_tour_length": int(self.options.all_cup_tour_length),
"mirror_200cc": int(self.options.mirror_200cc),
"lap_counts": lap_counts,
"character_item_total_weights": self.character_item_total_weights,
"global_items_total_weights": self.global_items_total_weights,
"item_boxes_as_locations": int(self.options.item_boxes_as_locations),
}
# Rerun Universal Tracker with received options.
@staticmethod
def interpret_slot_data(slot_data: dict[str:Any]) -> dict[str:Any]:
return slot_data
def launch_client():
from .mkdd_client import main
launch_subprocess(main, name="MKDD Client")
def add_client_to_launcher() -> None:
components.append(Component("Mario Kart Double Dash Client", func=launch_client))
add_client_to_launcher()