forked from mirror/Archipelago
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
287 lines
16 KiB
Python
287 lines
16 KiB
Python
from typing import TYPE_CHECKING
|
|
from BaseClasses import CollectionState
|
|
from ..generic.Rules import add_rule, set_rule
|
|
|
|
from .Locations import course_locations, Group, shared_hazard_locations, cup_locations
|
|
from .Options import Opt, GameMode, ShuffleDriftAbilities
|
|
from .Items import item_name_groups
|
|
|
|
if TYPE_CHECKING:
|
|
from . import MK64World
|
|
|
|
karts = item_name_groups["Karts"]
|
|
qualify_item_score_values = {
|
|
"Triple Red Shell Power": 1,
|
|
"Blue Shell Power": 0.5,
|
|
"Lightning Power": 1,
|
|
"Star Power": 1,
|
|
"Mushroom Power": 0.5,
|
|
"Triple Mushroom Power": 1,
|
|
"Super Mushroom Power": 1
|
|
}
|
|
win_item_score_values = {
|
|
"Red Shell Power": 0.5,
|
|
"Triple Red Shell Power": 1,
|
|
"Blue Shell Power": 0.5,
|
|
"Lightning Power": 0.5,
|
|
"Star Power": 0.5,
|
|
"Mushroom Power": 0.5,
|
|
"Triple Mushroom Power": 1,
|
|
"Super Mushroom Power": 0.5
|
|
}
|
|
|
|
|
|
def item_qualify_score(state: CollectionState, player: int) -> int: # 0 to 6
|
|
thing1 = [rating if state.has(item, player) else 0 for item, rating in qualify_item_score_values.items()]
|
|
thing2 = [rating if state.has("P2 " + item, player) else 0 for item, rating in qualify_item_score_values.items()]
|
|
sum1 = sum(thing1)
|
|
sum2 = sum(thing2)
|
|
maxsum = max(sum1, sum2)
|
|
return maxsum
|
|
|
|
|
|
def item_win_score(state: CollectionState, player: int) -> int: # 0 to 5
|
|
return max(sum([rating for item, rating in win_item_score_values.items() if state.has(item, player)]),
|
|
sum([rating for item, rating in win_item_score_values.items() if state.has("P2 " + item, player)]))
|
|
|
|
|
|
def kart_drift_score(state: CollectionState, player: int, opt: Opt, kart: str) -> int:
|
|
match opt.drift:
|
|
case ShuffleDriftAbilities.option_off:
|
|
return 2
|
|
case ShuffleDriftAbilities.option_free_drift:
|
|
return 1 + state.has("Progressive Drift " + kart, player)
|
|
case ShuffleDriftAbilities.option_free_mini_turbo:
|
|
return 2 * state.has("Progressive Drift " + kart, player)
|
|
case _: # ShuffleDriftAbilities.option_on or ShuffleDriftAbilities.plentiful
|
|
return min(state.count("Progressive Drift " + kart, player), 2)
|
|
|
|
|
|
def track_score(state: CollectionState, player: int, opt: Opt) -> int: # 0 to 2
|
|
return max((kart_drift_score(state, player, opt, kart) for kart in karts if state.has(kart, player)), default=0)
|
|
|
|
|
|
def off_road_score(state: CollectionState, player: int, opt: Opt) -> int: # 0 to 3
|
|
best = 0
|
|
for kart in karts:
|
|
if not state.has(kart, player):
|
|
continue
|
|
if opt.traction:
|
|
current = state.has("Off-Road Tires " + kart, player)
|
|
else:
|
|
current = 1
|
|
current += kart_drift_score(state, player, opt, kart)
|
|
best = max(best, current)
|
|
return best
|
|
|
|
|
|
def winter_score(state: CollectionState, player: int, opt: Opt) -> int: # 0 to 3
|
|
best = 0
|
|
for kart in karts:
|
|
if not state.has(kart, player):
|
|
continue
|
|
if (not opt.traction) or state.has("Winter Tires " + kart, player):
|
|
current = 2
|
|
elif state.has("Off-Road Tires " + kart, player):
|
|
current = 1
|
|
else:
|
|
current = 0
|
|
current += kart_drift_score(state, player, opt, kart)
|
|
best = max(best, current)
|
|
return best
|
|
|
|
|
|
def fence_score(state: CollectionState, player: int, opt: Opt) -> int: # 0 to 8 # TODO: Check for feather item boxes on course
|
|
if not opt.fences:
|
|
return 8
|
|
switch_ratings = [0, 4, 6, 7, 8]
|
|
feather_ratings = [2, 1, 0, 0, 0]
|
|
switch_count = state.count_group_unique("Switches", player)
|
|
return (switch_ratings[switch_count]
|
|
+ (state.has_any({"Feather Power", "P2 Feather Power"}, player) and feather_ratings[switch_count]))
|
|
|
|
|
|
def score_track_qualify(state: CollectionState, player: int, opt: Opt) -> int: # 0 to 16
|
|
return fence_score(state, player, opt) + track_score(state, player, opt) + item_qualify_score(state, player)
|
|
|
|
|
|
def score_off_road_qualify(state: CollectionState, player: int, opt: Opt) -> int: # 0 to 17
|
|
return fence_score(state, player, opt) + off_road_score(state, player, opt) + item_qualify_score(state, player)
|
|
|
|
|
|
def score_winter_qualify(state: CollectionState, player: int, opt: Opt) -> int: # 0 to 18
|
|
return fence_score(state, player, opt) + winter_score(state, player, opt) + item_qualify_score(state, player)
|
|
|
|
|
|
def score_track_win(state: CollectionState, player: int, opt: Opt) -> int: # 0 to 15
|
|
return fence_score(state, player, opt) + track_score(state, player, opt) + item_win_score(state, player)
|
|
|
|
|
|
def score_off_road_win(state: CollectionState, player: int, opt: Opt) -> int: # 0 to 16
|
|
return fence_score(state, player, opt) + off_road_score(state, player, opt) + item_win_score(state, player)
|
|
|
|
|
|
def score_winter_win(state: CollectionState, player: int, opt: Opt) -> int: # 0 to 17
|
|
return fence_score(state, player, opt) + winter_score(state, player, opt) + item_win_score(state, player)
|
|
|
|
|
|
course_qualify_rules = [ # TODO: Refactor with coupling among score types after more play testing & timing
|
|
# lambda: score threshhold <= fence_score + terrain score + item power score + optional railings score
|
|
lambda state, player, ease, opt: True, # Luigi Raceway
|
|
lambda state, player, ease, opt: ease - 1 <= score_track_qualify(state, player, opt), # Moo Moo Farm
|
|
lambda state, player, ease, opt: ease + 3 <= score_track_qualify(state, player, opt), # Koopa Troopa Beach
|
|
lambda state, player, ease, opt: (ease - 1 <= score_off_road_qualify(state, player, opt)) # Kalimari Desert
|
|
or (state.has("Yellow Switch", player)
|
|
and state.has_any({"Star Power", "P2 Star Power"}, player)),
|
|
lambda state, player, ease, opt: ease - 1 <= score_track_qualify(state, player, opt), # Toad's Turnpike
|
|
lambda state, player, ease, opt: ease + 4 <= score_winter_qualify(state, player, opt), # Frappe Snowland
|
|
lambda state, player, ease, opt: ease + 0 <= score_off_road_qualify(state, player, opt), # Choco Mountain
|
|
lambda state, player, ease, opt: ease + 0 <= score_off_road_qualify(state, player, opt), # Mario Raceway
|
|
lambda state, player, ease, opt: ease + 2 <= score_off_road_qualify(state, player, opt), # Wario Stadium
|
|
lambda state, player, ease, opt: ease + 2 <= score_winter_qualify(state, player, opt), # Sherbet Land
|
|
lambda state, player, ease, opt: ease + 3 <= score_off_road_qualify(state, player, opt), # Royal Raceway
|
|
lambda state, player, ease, opt: ease + 3 <= score_off_road_qualify(state, player, opt), # Bowser's Castle
|
|
lambda state, player, ease, opt: ease + 2 <= (score_off_road_qualify(state, player, opt) # D.K.'s Jungle Parkway
|
|
+ state.has("Railings D.K.'s Jungle Parkway", player)),
|
|
lambda state, player, ease, opt: ease + 5 <= (score_off_road_qualify(state, player, opt) # Yoshi Valley
|
|
+ state.has("Railings Yoshi Valley Main Track", player)
|
|
+ state.has("Railings Yoshi Valley Maze", player)),
|
|
lambda state, player, ease, opt: ease + 3 <= (score_track_qualify(state, player, opt) # Banshee Boardwalk
|
|
+ state.has("Railings Banshee Boardwalk North", player)
|
|
+ state.has("Railings Banshee Boardwalk South", player)),
|
|
lambda state, player, ease, opt: ease + 4 <= (score_track_qualify(state, player, opt) # Rainbow Road
|
|
+ 2 * state.has("Railings Rainbow Road 1", player)
|
|
+ 2 * state.has("Railings Rainbow Road 2", player)
|
|
+ 2 * state.has("Railings Rainbow Road 3", player)
|
|
+ state.has("Railings Rainbow Road 5", player))
|
|
]
|
|
|
|
course_win_rules = [ # TODO: Refactor with coupling among score types after more play testing & timing
|
|
# lambda: score threshhold <= fence_score + terrain score + item power score + optional railings score
|
|
lambda state, player, ease, opt: True, # Luigi Raceway
|
|
lambda state, player, ease, opt: ease + 1 <= score_track_win(state, player, opt), # Moo Moo Farm
|
|
lambda state, player, ease, opt: ease + 5 <= score_track_win(state, player, opt), # Koopa Troopa Beach
|
|
lambda state, player, ease, opt: (ease + 1 <= score_off_road_win(state, player, opt)) # Kalimari Desert
|
|
or (state.has("Yellow Switch", player)
|
|
and state.has_any({"Star Power", "P2 Star Power"}, player)),
|
|
lambda state, player, ease, opt: ease + 1 <= score_track_win(state, player, opt), # Toad's Turnpike
|
|
lambda state, player, ease, opt: ease + 6 <= score_winter_win(state, player, opt), # Frappe Snowland
|
|
lambda state, player, ease, opt: ease + 3 <= score_off_road_win(state, player, opt), # Choco Mountain
|
|
lambda state, player, ease, opt: ease + 3 <= score_off_road_win(state, player, opt), # Mario Raceway
|
|
lambda state, player, ease, opt: ease + 4 <= score_off_road_win(state, player, opt), # Wario Stadium
|
|
lambda state, player, ease, opt: ease + 4 <= score_winter_win(state, player, opt), # Sherbet Land
|
|
lambda state, player, ease, opt: ease + 5 <= score_off_road_win(state, player, opt), # Royal Raceway
|
|
lambda state, player, ease, opt: ease + 5 <= score_off_road_win(state, player, opt), # Bowser's Castle
|
|
lambda state, player, ease, opt: ease + 4 <= (score_off_road_win(state, player, opt) # D.K.'s Jungle Parkway
|
|
+ state.has("Railings D.K.'s Jungle Parkway", player)),
|
|
lambda state, player, ease, opt: ease + 7 <= (score_off_road_win(state, player, opt) # Yoshi Valley
|
|
+ state.has("Railings Yoshi Valley Main Track", player)
|
|
+ state.has("Railings Yoshi Valley Maze", player)),
|
|
lambda state, player, ease, opt: ease + 5 <= (score_track_win(state, player, opt) # Banshee Boardwalk
|
|
+ state.has("Railings Banshee Boardwalk North", player)
|
|
+ state.has("Railings Banshee Boardwalk South", player)),
|
|
lambda state, player, ease, opt: ease + 6 <= (score_track_win(state, player, opt) # Rainbow Road
|
|
+ 2 * state.has("Railings Rainbow Road 1", player)
|
|
+ 2 * state.has("Railings Rainbow Road 2", player)
|
|
+ 2 * state.has("Railings Rainbow Road 3", player)
|
|
+ state.has("Railings Rainbow Road 5", player))
|
|
]
|
|
|
|
|
|
def can_win_trophy(state: CollectionState, player: int,
|
|
courses: frozenset[int], trophy_class: int, ease: int, opt: Opt) -> bool:
|
|
return trophy_class <= sum(course_win_rules[course](state, player, ease, opt) for course in courses)
|
|
|
|
|
|
def set_star_access_rule(world: "MK64World", loc_name: str, player: int, opt: Opt) -> None:
|
|
# Relevant Option
|
|
if opt.two_player:
|
|
set_rule(world.get_location(loc_name),
|
|
lambda state: state.has_any({"Star Power", "P2 Star Power"}, player))
|
|
else:
|
|
set_rule(world.get_location(loc_name), lambda state: state.has("Star Power", player))
|
|
|
|
|
|
def create_rules(world: "MK64World") -> None:
|
|
multiworld = world.multiworld
|
|
player = world.player
|
|
opt = world.opt
|
|
order = world.course_order
|
|
|
|
# Make starting kart(s) required, which has them show up in the spoiler log Playthrough.
|
|
if opt.mode == GameMode.option_cups:
|
|
first_entrance = world.multiworld.get_region("Menu", player).get_exits()[0]
|
|
if len(world.starting_karts) == 1:
|
|
first_entrance.access_rule = lambda state, kart=world.starting_karts[0]: state.has(kart, player)
|
|
else:
|
|
first_entrance.access_rule = lambda state, kts=frozenset(world.starting_karts): state.has_all(kts, player)
|
|
|
|
# Region (Entrance) Rules (handled in Regions.py instead for now)
|
|
# if opt_game_mode == GameMode.option_cups:
|
|
# set_rule(multiworld.get_entrance("Flower Cup 1", player),
|
|
# lambda state: state.has("Progressive Cup Unlock", player, 1))
|
|
# set_rule(multiworld.get_entrance("Star Cup 1", player),
|
|
# lambda state: state.has("Progressive Cup Unlock", player, 2))
|
|
# set_rule(multiworld.get_entrance("Special Cup 1", player),
|
|
# lambda state: state.has("Progressive Cup Unlock", player, 3))
|
|
# elif opt_game_mode == GameMode.option_courses:
|
|
# pass
|
|
|
|
# Base Course Rules # TODO: Clean this up, probably combine with Star Access Rules section
|
|
for locations in course_locations.values():
|
|
for name, (code, group) in locations.items():
|
|
if group == Group.base:
|
|
if code % 3 < 2:
|
|
set_rule(world.get_location(name),
|
|
lambda state, c=code: course_win_rules[(c - 4660000) // 3](state, player, opt.logic, opt))
|
|
else:
|
|
set_rule(world.get_location(name),
|
|
lambda state, c=code: course_qualify_rules[(c - 4660000) // 3](state, player, opt.logic, opt))
|
|
|
|
# Item Spot Access Rules moved to Regions.py for context that knows which item box spots to apply rules to
|
|
|
|
# Koopa Troopa Beach Rock Access
|
|
if opt.special_boxes and opt.fences:
|
|
set_rule(world.get_location("Koopa Troopa Beach Rock"),
|
|
lambda state: state.has_all({"Yellow Switch", "Blue Switch"}, player)
|
|
or state.has_all({"Red Switch", "Green Switch"}, player))
|
|
|
|
if opt.secrets and opt.fences:
|
|
# Kalimari Desert Secret Access
|
|
set_rule(world.get_location("Kalimari Desert Secret"),
|
|
lambda state: state.has_any({"Yellow Switch", "Red Switch", "Blue Switch",
|
|
"Feather Power", "P2 Feather Power"}, player))
|
|
|
|
# Marty's Secret Access
|
|
set_rule(world.get_location("Marty's Secret"),
|
|
lambda state: state.has_any({"Green Switch", "Feather Power", "P2 Feather Power"}, player))
|
|
|
|
# Hazard Access, all use Star Power
|
|
if opt.hazards:
|
|
for locations in course_locations.values():
|
|
for name, (_, group) in locations.items():
|
|
if group == Group.hazard:
|
|
set_star_access_rule(world, name, player, opt)
|
|
for name, _ in shared_hazard_locations.items():
|
|
set_star_access_rule(world, name, player, opt)
|
|
|
|
# Add Blue Fence rule to Mario sign
|
|
if opt.fences:
|
|
add_rule(world.get_location("Destroy Mario Sign"), lambda state: state.has("Blue Switch", player))
|
|
|
|
# Cup Trophy Rules
|
|
cup_courses = [frozenset(order[i:i + 4]) for i in range(0, len(order), 4)]
|
|
if opt.mode == GameMode.option_cups:
|
|
trophy_class_mapping = {"Bronze": 1, "Silver": 2, "Gold": 3}
|
|
engine_class_mapping = {"100cc": 2, "150cc": 3} # 50cc is 0
|
|
for locations, courses in zip(cup_locations.values(), cup_courses):
|
|
for loc_name in locations.keys():
|
|
difficulty, trophy = loc_name.rsplit(" ", 2)[-2:]
|
|
trophy_class = trophy_class_mapping[trophy]
|
|
ease = opt.logic + engine_class_mapping.get(difficulty, 0)
|
|
set_rule(world.get_location(loc_name),
|
|
lambda state, c=courses, t=trophy_class, e=ease: can_win_trophy(state, player, c, t, e, opt))
|
|
|
|
# Completion Condition (Victory Rule)
|
|
multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
|