Archipelago/worlds/zillion/options.py

392 lines
9.7 KiB
Python

from collections import Counter
from dataclasses import dataclass
from typing import ClassVar, Dict, Tuple
from typing_extensions import TypeGuard # remove when Python >= 3.10
from Options import DefaultOnToggle, NamedRange, PerGameCommonOptions, Range, Toggle, Choice
from zilliandomizer.options import (
Options as ZzOptions, char_to_gun, char_to_jump, ID,
VBLR as ZzVBLR, Chars, ItemCounts as ZzItemCounts
)
from zilliandomizer.options.parsing import validate as zz_validate
class ZillionContinues(NamedRange):
"""
number of continues before game over
game over teleports you to your ship, keeping items and open doors
"""
default = 3
range_start = 0
range_end = 21
display_name = "continues"
special_range_names = {
"vanilla": 3,
"infinity": 21
}
class ZillionFloppyReq(Range):
""" how many floppy disks are required """
range_start = 0
range_end = 8
default = 5
display_name = "floppies required"
class VBLR(Choice):
option_vanilla = 0
option_balanced = 1
option_low = 2
option_restrictive = 3
default = 1
def to_zz_vblr(self) -> ZzVBLR:
def is_vblr(o: str) -> TypeGuard[ZzVBLR]:
"""
This function is because mypy doesn't support narrowing with `in`,
https://github.com/python/mypy/issues/12535
so this is the only way I see to get type narrowing to `Literal`.
"""
return o in ("vanilla", "balanced", "low", "restrictive")
key = self.current_key
assert is_vblr(key), f"{key=}"
return key
class ZillionGunLevels(VBLR):
"""
Zillion gun power for the number of Zillion power ups you pick up
For "restrictive", Champ is the only one that can get Zillion gun power level 3.
"""
display_name = "gun levels"
class ZillionJumpLevels(VBLR):
"""
jump levels for each character level
For "restrictive", Apple is the only one that can get jump level 3.
"""
display_name = "jump levels"
class ZillionRandomizeAlarms(DefaultOnToggle):
""" whether to randomize the locations of alarm sensors """
display_name = "randomize alarms"
class ZillionMaxLevel(Range):
""" the highest level you can get """
range_start = 3
range_end = 8
default = 8
display_name = "max level"
class ZillionOpasPerLevel(Range):
"""
how many Opa-Opas are required to level up
Lower makes you level up faster.
"""
range_start = 1
range_end = 5
default = 2
display_name = "Opa-Opas per level"
class ZillionStartChar(Choice):
""" which character you start with """
option_jj = 0
option_apple = 1
option_champ = 2
display_name = "start character"
default = "random"
_name_capitalization: ClassVar[Dict[int, Chars]] = {
option_jj: "JJ",
option_apple: "Apple",
option_champ: "Champ",
}
def get_char(self) -> Chars:
return ZillionStartChar._name_capitalization[self.value]
class ZillionIDCardCount(Range):
"""
how many ID Cards are in the game
Vanilla is 63
maximum total for all items is 144
"""
range_start = 0
range_end = 126
default = 42
display_name = "ID Card count"
class ZillionBreadCount(Range):
"""
how many Breads are in the game
Vanilla is 33
maximum total for all items is 144
"""
range_start = 0
range_end = 126
default = 50
display_name = "Bread count"
class ZillionOpaOpaCount(Range):
"""
how many Opa-Opas are in the game
Vanilla is 26
maximum total for all items is 144
"""
range_start = 0
range_end = 126
default = 26
display_name = "Opa-Opa count"
class ZillionZillionCount(Range):
"""
how many Zillion gun power ups are in the game
Vanilla is 6
maximum total for all items is 144
"""
range_start = 0
range_end = 126
default = 8
display_name = "Zillion power up count"
class ZillionFloppyDiskCount(Range):
"""
how many Floppy Disks are in the game
Vanilla is 5
maximum total for all items is 144
"""
range_start = 0
range_end = 126
default = 7
display_name = "Floppy Disk count"
class ZillionScopeCount(Range):
"""
how many Scopes are in the game
Vanilla is 4
maximum total for all items is 144
"""
range_start = 0
range_end = 126
default = 4
display_name = "Scope count"
class ZillionRedIDCardCount(Range):
"""
how many Red ID Cards are in the game
Vanilla is 1
maximum total for all items is 144
"""
range_start = 0
range_end = 126
default = 2
display_name = "Red ID Card count"
class ZillionEarlyScope(Toggle):
""" make sure Scope is available early """
display_name = "early scope"
class ZillionSkill(Range):
"""
the difficulty level of the game
higher skill:
- can require more precise platforming movement
- lowers your defense
- gives you less time to escape at the end
"""
range_start = 0
range_end = 5
default = 2
class ZillionStartingCards(NamedRange):
"""
how many ID Cards to start the game with
Refilling at the ship also ensures you have at least this many cards.
0 gives vanilla behavior.
"""
default = 2
range_start = 0
range_end = 10
display_name = "starting cards"
special_range_names = {
"vanilla": 0
}
class ZillionRoomGen(Toggle):
""" whether to generate rooms with random terrain """
display_name = "room generation"
@dataclass
class ZillionOptions(PerGameCommonOptions):
continues: ZillionContinues
floppy_req: ZillionFloppyReq
gun_levels: ZillionGunLevels
jump_levels: ZillionJumpLevels
randomize_alarms: ZillionRandomizeAlarms
max_level: ZillionMaxLevel
start_char: ZillionStartChar
opas_per_level: ZillionOpasPerLevel
id_card_count: ZillionIDCardCount
bread_count: ZillionBreadCount
opa_opa_count: ZillionOpaOpaCount
zillion_count: ZillionZillionCount
floppy_disk_count: ZillionFloppyDiskCount
scope_count: ZillionScopeCount
red_id_card_count: ZillionRedIDCardCount
early_scope: ZillionEarlyScope
skill: ZillionSkill
starting_cards: ZillionStartingCards
room_gen: ZillionRoomGen
def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts:
tr: ZzItemCounts = {
ID.card: ic["ID Card"],
ID.red: ic["Red ID Card"],
ID.floppy: ic["Floppy Disk"],
ID.bread: ic["Bread"],
ID.gun: ic["Zillion"],
ID.opa: ic["Opa-Opa"],
ID.scope: ic["Scope"],
ID.empty: ic["Empty"],
}
return tr
def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]":
"""
adjusts options to make game completion possible
`options` parameter is ZillionOptions object that was put on my world by the core
"""
skill = options.skill.value
jump_option = options.jump_levels.to_zz_vblr()
required_level = char_to_jump["Apple"][jump_option].index(3) + 1
if skill == 0:
# because of hp logic on final boss
required_level = 8
gun_option = options.gun_levels.to_zz_vblr()
guns_required = char_to_gun["Champ"][gun_option].index(3)
floppy_req = options.floppy_req
item_counts = Counter({
"ID Card": options.id_card_count,
"Bread": options.bread_count,
"Opa-Opa": options.opa_opa_count,
"Zillion": options.zillion_count,
"Floppy Disk": options.floppy_disk_count,
"Scope": options.scope_count,
"Red ID Card": options.red_id_card_count
})
minimums = Counter({
"ID Card": 0,
"Bread": 0,
"Opa-Opa": required_level - 1,
"Zillion": guns_required,
"Floppy Disk": floppy_req.value,
"Scope": 0,
"Red ID Card": 1
})
for key in minimums:
item_counts[key] = max(minimums[key], item_counts[key])
max_movables = 144 - sum(minimums.values())
movables = item_counts - minimums
while sum(movables.values()) > max_movables:
# logging.warning("zillion options validate: player options item counts too high")
total = sum(movables.values())
scaler = max_movables / total
for key in movables:
movables[key] = int(movables[key] * scaler)
item_counts = movables + minimums
# now have required items, and <= 144
# now fill remaining with empty
total = sum(item_counts.values())
diff = 144 - total
if "Empty" not in item_counts:
item_counts["Empty"] = 0
item_counts["Empty"] += diff
assert sum(item_counts.values()) == 144
max_level = options.max_level
max_level.value = max(required_level, max_level.value)
opas_per_level = options.opas_per_level
while (opas_per_level.value > 1) and (1 + item_counts["Opa-Opa"] // opas_per_level.value < max_level.value):
# logging.warning(
# "zillion options validate: option opas_per_level incompatible with options max_level and opa_opa_count"
# )
opas_per_level.value -= 1
# that should be all of the level requirements met
starting_cards = options.starting_cards
room_gen = options.room_gen
zz_item_counts = convert_item_counts(item_counts)
zz_op = ZzOptions(
zz_item_counts,
jump_option,
gun_option,
opas_per_level.value,
max_level.value,
False, # tutorial
skill,
options.start_char.get_char(),
floppy_req.value,
options.continues.value,
bool(options.randomize_alarms.value),
bool(options.early_scope.value),
True, # balance defense
starting_cards.value,
bool(room_gen.value)
)
zz_validate(zz_op)
return zz_op, item_counts