418 lines
10 KiB
Python
418 lines
10 KiB
Python
from collections import Counter
|
|
from dataclasses import dataclass
|
|
from typing import ClassVar, Dict, Literal, Tuple
|
|
from typing_extensions import TypeGuard # remove when Python >= 3.10
|
|
|
|
from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Removed, Toggle
|
|
|
|
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 ZillionMapGen(Choice):
|
|
"""
|
|
- none: vanilla map
|
|
- rooms: random terrain inside rooms, but path through base is vanilla
|
|
- full: random path through base
|
|
"""
|
|
display_name = "map generation"
|
|
option_none = 0
|
|
option_rooms = 1
|
|
option_full = 2
|
|
default = 0
|
|
|
|
def zz_value(self) -> Literal['none', 'rooms', 'full']:
|
|
if self.value == ZillionMapGen.option_none:
|
|
return "none"
|
|
if self.value == ZillionMapGen.option_rooms:
|
|
return "rooms"
|
|
assert self.value == ZillionMapGen.option_full
|
|
return "full"
|
|
|
|
|
|
@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
|
|
map_gen: ZillionMapGen
|
|
|
|
room_gen: Removed
|
|
|
|
|
|
z_option_groups = [
|
|
OptionGroup("item counts", [
|
|
ZillionIDCardCount, ZillionBreadCount, ZillionOpaOpaCount, ZillionZillionCount,
|
|
ZillionFloppyDiskCount, ZillionScopeCount, ZillionRedIDCardCount
|
|
])
|
|
]
|
|
|
|
|
|
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
|
|
|
|
map_gen = options.map_gen.zz_value()
|
|
|
|
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,
|
|
map_gen
|
|
)
|
|
zz_validate(zz_op)
|
|
return zz_op, item_counts
|