mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-30 08:43:20 -07:00
413 lines
20 KiB
Python
413 lines
20 KiB
Python
import math
|
|
import logging
|
|
|
|
from BaseClasses import Region, Entrance, Item, Tutorial, CollectionState
|
|
from .Items import YachtDiceItem, item_table, item_groups
|
|
from .Locations import YachtDiceLocation, all_locations, ini_locations
|
|
from .Options import YachtDiceOptions
|
|
from .Rules import set_yacht_rules, set_yacht_completion_rules, dice_simulation
|
|
from ..AutoWorld import World, WebWorld
|
|
|
|
|
|
class YachtDiceWeb(WebWorld):
|
|
tutorials = [Tutorial(
|
|
"Multiworld Setup Guide",
|
|
"A guide to setting up Yacht Dice. This guide covers "
|
|
"single-player, multiworld, and website.",
|
|
"English",
|
|
"setup_en.md",
|
|
"setup/en",
|
|
["Spineraks"]
|
|
)]
|
|
|
|
class YachtDiceWorld(World):
|
|
"""
|
|
Yacht Dice is a straightforward game, custom-made for Archipelago,
|
|
where you cast your dice to chart a course for high scores,
|
|
unlocking valuable treasures along the way.
|
|
Discover more dice, extra rolls, multipliers,
|
|
and unlockable categories to navigate the depths of the game.
|
|
Roll your way to victory by reaching the target score!
|
|
"""
|
|
game: str = "Yacht Dice"
|
|
options_dataclass = YachtDiceOptions
|
|
|
|
web = YachtDiceWeb()
|
|
|
|
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
|
|
|
location_name_to_id = {name: data.id for name, data in all_locations.items()}
|
|
|
|
item_name_groups = item_groups
|
|
|
|
ap_world_version = "2.0.4"
|
|
|
|
def _get_yachtdice_data(self):
|
|
return {
|
|
# "world_seed": self.multiworld.per_slot_randoms[self.player].getrandbits(32),
|
|
"seed_name": self.multiworld.seed_name,
|
|
"player_name": self.multiworld.get_player_name(self.player),
|
|
"player_id": self.player,
|
|
"race": self.multiworld.is_race,
|
|
}
|
|
|
|
#In generate early, we fill the item-pool, then determine the number of locations, and add filler items.
|
|
def generate_early(self):
|
|
self.itempool = []
|
|
self.precollected = []
|
|
|
|
#number of dice and rolls in the pull
|
|
ind_dice_rolls = self.options.minimal_number_of_dice_and_rolls.value
|
|
|
|
num_of_dice = [0, 2, 5, 5, 6, 7, 8][ind_dice_rolls]
|
|
num_of_rolls = [0, 2, 3, 5, 4, 3, 2][ind_dice_rolls]
|
|
|
|
#amount of dice and roll fragments needed to get a dice or roll
|
|
frags_per_dice = self.options.number_of_dice_fragments_per_dice.value
|
|
frags_per_roll = self.options.number_of_roll_fragments_per_roll.value
|
|
|
|
#count number of plando items not from pool, we need extra locations for them
|
|
self.extra_plando_items = 0
|
|
|
|
for plando_setting in self.multiworld.plando_items[self.player]:
|
|
if plando_setting.get("from_pool", False) is False:
|
|
self.extra_plando_items += sum(value for value in plando_setting["items"].values())
|
|
|
|
# Create a list with the specified number of 1s
|
|
num_ones = self.options.alternative_categories.value
|
|
categorylist = [1] * num_ones + [0] * (16 - num_ones)
|
|
|
|
# Shuffle the list to randomize the order
|
|
self.multiworld.random.shuffle(categorylist)
|
|
|
|
#A list of all possible categories.
|
|
#Every entry in the list has two categories, one 'default' category and one 'alt'.
|
|
#You get either of the two for every entry, so a total of 16 unique categories.
|
|
all_categories = [
|
|
["Category Choice", "Category Double Threes and Fours"],
|
|
["Category Inverse Choice", "Category Quadruple Ones and Twos"],
|
|
["Category Ones", "Category Distincts"],
|
|
["Category Twos", "Category Two times Ones"],
|
|
["Category Threes", "Category Half of Sixes"],
|
|
["Category Fours", "Category Twos and Threes"],
|
|
["Category Fives", "Category Sum of Odds"],
|
|
["Category Sixes", "Category Sum of Evens"],
|
|
["Category Pair", "Category Micro Straight"],
|
|
["Category Three of a Kind", "Category Three Odds"],
|
|
["Category Four of a Kind", "Category 1-2-1 Consecutive"],
|
|
["Category Tiny Straight", "Category Three Distinct Dice"],
|
|
["Category Small Straight", "Category Two Pair"],
|
|
["Category Large Straight", "Category 2-1-2 Consecutive"],
|
|
["Category Full House", "Category Five Distinct Dice"],
|
|
["Category Yacht", "Category 4&5 Full House"]
|
|
]
|
|
|
|
#categories used in this game.
|
|
possible_categories = []
|
|
|
|
for index, cats in enumerate(all_categories):
|
|
possible_categories += [cats[categorylist[index]]]
|
|
|
|
# Add Choice and Inverse choice (or their alts) to the precollected list.
|
|
if index == 0 or index == 1:
|
|
self.precollected += [cats[categorylist[index]]]
|
|
else:
|
|
self.itempool += [cats[categorylist[index]]]
|
|
|
|
# Also start with one Roll and one Dice
|
|
self.precollected += ["Roll"]
|
|
self.precollected += ["Dice"]
|
|
|
|
#if one fragment per dice, just add "Dice" objects
|
|
if frags_per_dice == 1:
|
|
self.itempool += ["Dice"] * (num_of_dice-1) #minus one because one is in start inventory
|
|
else:
|
|
self.itempool += ["Dice"] #always add a full dice to make generation easier (will be early)
|
|
self.itempool += ["Dice Fragment"] * (frags_per_dice * (num_of_dice-2))
|
|
|
|
#if one fragment per roll, just add "Roll" objects
|
|
if frags_per_roll == 1:
|
|
self.itempool += ["Roll"] * (num_of_rolls-1) #minus one because one is in start inventory
|
|
else:
|
|
self.itempool += ["Roll"] #always add a full roll to make generation easier (will be early)
|
|
self.itempool += ["Roll Fragment"] * (frags_per_roll * (num_of_rolls-2))
|
|
|
|
already_items = len(self.itempool) + self.extra_plando_items
|
|
|
|
#Yacht Dice needs extra filler items so it doesn't get stuck in generation.
|
|
#For now, we calculate the number of extra items we'll need later.
|
|
if self.options.minimize_extra_items.value:
|
|
extraPercentage = max(0.1, 0.8 - self.multiworld.players / 10)
|
|
else:
|
|
extraPercentage = 0.7
|
|
extra_locations_needed = max(10, math.ceil(already_items * extraPercentage))
|
|
|
|
#max score is the value of the last check. Goal score is the score needed to 'finish' the game
|
|
self.max_score = self.options.score_for_last_check.value
|
|
self.goal_score = min(self.max_score, self.options.score_for_goal.value)
|
|
|
|
#Yacht Dice adds items into the pool until a score of at least 1000 is reached.
|
|
#the yaml contains weights, which determine how likely it is that specific items get added.
|
|
#If all weights are 0, some of them will be made to be non-zero later.
|
|
weights = [
|
|
self.options.weight_of_dice.value,
|
|
self.options.weight_of_roll.value,
|
|
self.options.weight_of_fixed_score_multiplier.value,
|
|
self.options.weight_of_step_score_multiplier.value,
|
|
self.options.weight_of_double_category.value,
|
|
self.options.weight_of_points.value
|
|
]
|
|
|
|
#if the player wants extra rolls or dice, fill the pool with fragments until close to an extra roll/dice
|
|
if weights[0] > 0 and frags_per_dice > 1:
|
|
self.itempool += ["Dice Fragment"] * (frags_per_dice - 1)
|
|
if weights[1] > 0 and frags_per_roll > 1:
|
|
self.itempool += ["Roll Fragment"] * (frags_per_roll - 1)
|
|
|
|
#calibrate the weights, since the impact of each of the items is different
|
|
weights[0] = weights[0] / 5 * frags_per_dice
|
|
weights[1] = weights[1] / 5 * frags_per_roll
|
|
|
|
extra_points_added = 0
|
|
multipliers_added = 0
|
|
|
|
#Keep adding items until a score of 1000 is in logic
|
|
while dice_simulation(self.itempool + self.precollected, "state_is_a_list", self.options) < 1000:
|
|
all_items = self.itempool + self.precollected
|
|
dice_fragments_in_pool = all_items.count("Dice") * frags_per_dice + all_items.count("Dice Fragment")
|
|
if dice_fragments_in_pool + 1 >= 9 * frags_per_dice:
|
|
weights[0] = 0 #don't allow >=9 dice
|
|
roll_fragments_in_pool = all_items.count("Roll") * frags_per_roll + all_items.count("Roll Fragment")
|
|
if roll_fragments_in_pool + 1 >= 6 * frags_per_roll:
|
|
weights[1] = 0 # don't allow >= 6 rolls
|
|
|
|
#Don't allow too many multipliers
|
|
if multipliers_added > 50:
|
|
weights[2] = 0
|
|
weights[3] = 0
|
|
|
|
#Don't allow too many extra points
|
|
if extra_points_added > 300:
|
|
weights[5] = 0
|
|
|
|
#if all weights are zero, allow to add fixed score multiplier, double category, points.
|
|
if sum(weights) == 0:
|
|
if multipliers_added <= 50:
|
|
weights[2] = 1
|
|
weights[4] = 1
|
|
if extra_points_added <= 300:
|
|
weights[5] = 1
|
|
|
|
#Next, add the appropriate item. We'll slightly alter weights to avoid too many of the same item
|
|
which_item_to_add = self.multiworld.random.choices([0,1,2,3,4,5], weights = weights)[0]
|
|
if which_item_to_add == 0:
|
|
if frags_per_dice == 1:
|
|
self.itempool += ["Dice"]
|
|
else:
|
|
self.itempool += ["Dice Fragment"]
|
|
weights[0] /= (1 + frags_per_dice)
|
|
elif which_item_to_add == 1:
|
|
if frags_per_roll == 1:
|
|
self.itempool += ["Roll"]
|
|
else:
|
|
self.itempool += ["Roll Fragment"]
|
|
weights[1] /= (1 + frags_per_roll)
|
|
elif which_item_to_add == 2:
|
|
self.itempool += ["Fixed Score Multiplier"]
|
|
weights[2] /= 1.05
|
|
multipliers_added += 1
|
|
elif which_item_to_add == 3:
|
|
self.itempool += ["Step Score Multiplier"]
|
|
weights[3] /= 1.1
|
|
multipliers_added += 1
|
|
elif which_item_to_add == 4:
|
|
cat_weights = [2, 2, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1]
|
|
self.itempool += self.multiworld.random.choices(possible_categories, weights = cat_weights)
|
|
weights[4] /= 1.1
|
|
elif which_item_to_add == 5:
|
|
score_dist = self.options.points_size.value
|
|
probs = [1,0,0]
|
|
if score_dist == 1:
|
|
probs = [0.9,0.08,0]
|
|
if score_dist == 2:
|
|
probs = [0,1,0]
|
|
if score_dist == 3:
|
|
probs = [0,0.3,0.7]
|
|
if score_dist == 4:
|
|
probs = [0.3,0.4,0.3]
|
|
c = self.multiworld.random.choices([0,1,2], weights = probs)[0]
|
|
if c == 0:
|
|
self.itempool += ["1 Point"]
|
|
extra_points_added += 1
|
|
weights[5] /= 1.01
|
|
elif c==1:
|
|
self.itempool += ["10 Points"]
|
|
extra_points_added += 10
|
|
weights[5] /= 1.1
|
|
elif c==2:
|
|
self.itempool += ["100 Points"]
|
|
extra_points_added += 100
|
|
weights[5] /= 2
|
|
else:
|
|
raise Exception("Unknown point value (Yacht Dice)")
|
|
else:
|
|
raise Exception("Invalid index when adding new items in Yacht Dice")
|
|
|
|
#count the number of locations in the game.
|
|
already_items = len(self.itempool) + self.extra_plando_items + 1 #+1 because of Victory item
|
|
|
|
#We need to add more filler/useful items if there are many items in the pool to guarantee succesful generation
|
|
extra_locations_needed += (already_items - 45) // 15
|
|
self.number_of_locations = already_items + extra_locations_needed
|
|
|
|
# From here, we will count the number of items in the self.itempool, and add usuful/filler items to the pool,
|
|
# making sure not to exceed the number of locations.
|
|
|
|
#first, we flood the entire pool with extra points (useful), if that setting is chosen.
|
|
if self.options.add_bonus_points.value == 1: #all of the extra points
|
|
already_items = len(self.itempool) + self.extra_plando_items + 1
|
|
self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 100)
|
|
|
|
#second, we flood the entire pool with story chapters (filler), if that setting is chosen.
|
|
if self.options.add_story_chapters.value == 1: #all of the story chapters
|
|
already_items = len(self.itempool) + self.extra_plando_items + 1
|
|
number_of_items = min(self.number_of_locations - already_items, 100)
|
|
number_of_items = (number_of_items // 10) * 10 #story chapters always come in multiples of 10
|
|
self.itempool += ["Story Chapter"] * number_of_items
|
|
|
|
#add some extra points (useful)
|
|
if self.options.add_bonus_points.value == 2: #add extra points if wanted
|
|
already_items = len(self.itempool) + self.extra_plando_items + 1
|
|
self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 10)
|
|
|
|
#add some story chapters (filler)
|
|
if self.options.add_story_chapters.value == 2: #add extra points if wanted
|
|
already_items = len(self.itempool) + self.extra_plando_items + 1
|
|
if(self.number_of_locations - already_items >= 10):
|
|
self.itempool += ["Story Chapter"] * 10
|
|
|
|
#add some more extra points if there is still room
|
|
if self.options.add_bonus_points.value == 2:
|
|
already_items = len(self.itempool) + self.extra_plando_items + 1
|
|
self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 10)
|
|
|
|
#add some encouragements filler-items if there is still room
|
|
already_items = len(self.itempool) + self.extra_plando_items + 1
|
|
self.itempool += ["Encouragement"] * min(self.number_of_locations - already_items, 5)
|
|
|
|
#add some fun facts filler-items if there is still room
|
|
already_items = len(self.itempool) + self.extra_plando_items + 1
|
|
self.itempool += ["Fun Fact"] * min(self.number_of_locations - already_items, 5)
|
|
|
|
#finally, add some "Good RNG" and "Bad RNG" items to complete the item pool
|
|
#these items are filler and do not do anything.
|
|
|
|
#probability of Good and Bad rng, based on difficulty for fun :)
|
|
p = 1.1 - 0.25 * self.options.game_difficulty.value
|
|
already_items = len(self.itempool) + self.extra_plando_items + 1
|
|
self.itempool += self.multiworld.random.choices(
|
|
["Good RNG", "Bad RNG"],
|
|
weights=[p, 1-p],
|
|
k=self.number_of_locations - already_items
|
|
)
|
|
|
|
#we are done adding items. Now because of the last step, number of items should be number of locations
|
|
already_items = len(self.itempool) + self.extra_plando_items + 1
|
|
if already_items != self.number_of_locations:
|
|
raise Exception(f"[Yacht Dice] Number in self.itempool is not number of locations "
|
|
f"{already_items} {self.number_of_locations}.")
|
|
|
|
#add precollected items using push_precollected. Items in self.itempool get created in create_items
|
|
for item in self.precollected:
|
|
self.multiworld.push_precollected(self.create_item(item))
|
|
|
|
#make sure one dice and one roll is early, so that you will have 2 dice and 2 rolls soon
|
|
self.multiworld.early_items[self.player]["Dice"] = 1
|
|
self.multiworld.early_items[self.player]["Roll"] = 1
|
|
|
|
def create_items(self):
|
|
self.multiworld.itempool += [self.create_item(name) for name in self.itempool]
|
|
|
|
def create_regions(self):
|
|
#call the ini_locations function, that generates locations based on the inputs.
|
|
location_table, goal_index = ini_locations(self.goal_score, self.max_score, self.number_of_locations,
|
|
self.options.game_difficulty.value)
|
|
|
|
#simple menu-board construction
|
|
menu = Region("Menu", self.player, self.multiworld)
|
|
board = Region("Board", self.player, self.multiworld)
|
|
|
|
#add locations to board, one for every location in the location_table
|
|
board.locations = [YachtDiceLocation(self.player, loc_name, loc_data.score, loc_data.id, board)
|
|
for loc_name, loc_data in location_table.items() if loc_data.region == board.name]
|
|
|
|
#which index of all locations should have the Victory item.
|
|
|
|
# Add the victory item to the correct location.
|
|
# The website declares that the game is complete when the victory item is obtained.
|
|
board.locations[goal_index].place_locked_item(self.create_item("Victory"))
|
|
|
|
#these will be slot_data input
|
|
self.goal_score = board.locations[goal_index].yacht_dice_score
|
|
self.max_score = board.locations[-1].yacht_dice_score
|
|
|
|
#add the regions
|
|
connection = Entrance(self.player, "New Board", menu)
|
|
menu.exits.append(connection)
|
|
connection.connect(board)
|
|
self.multiworld.regions += [menu, board]
|
|
|
|
def set_rules(self):
|
|
#set rules per location, and add the rule for beating the game
|
|
set_yacht_rules(self.multiworld, self.player, self.options)
|
|
set_yacht_completion_rules(self.multiworld, self.player)
|
|
|
|
def fill_slot_data(self):
|
|
#make slot data, which consists of yachtdice_data, options, and some other variables.
|
|
yacht_dice_data = self._get_yachtdice_data()
|
|
yacht_dice_options = self.options.as_dict(
|
|
"game_difficulty",
|
|
"score_for_last_check",
|
|
"score_for_goal",
|
|
"minimal_number_of_dice_and_rolls",
|
|
"number_of_dice_fragments_per_dice",
|
|
"number_of_roll_fragments_per_roll",
|
|
"alternative_categories",
|
|
"weight_of_dice",
|
|
"weight_of_roll",
|
|
"weight_of_fixed_score_multiplier",
|
|
"weight_of_step_score_multiplier",
|
|
"weight_of_double_category",
|
|
"weight_of_points",
|
|
"points_size",
|
|
"minimize_extra_items",
|
|
"add_bonus_points",
|
|
"add_story_chapters",
|
|
"which_story",
|
|
"allow_manual_input"
|
|
)
|
|
slot_data = {**yacht_dice_data, **yacht_dice_options} #combine the two
|
|
slot_data["goal_score"] = self.goal_score
|
|
slot_data["last_check_score"] = self.max_score
|
|
slot_data["ap_world_version"] = self.ap_world_version
|
|
return slot_data
|
|
|
|
def create_item(self, name: str) -> Item:
|
|
item_data = item_table[name]
|
|
item = YachtDiceItem(name, item_data.classification, item_data.code, self.player)
|
|
return item
|
|
|
|
def collect(self, state: CollectionState, item: Item) -> bool:
|
|
change = super().collect(state, item)
|
|
if change:
|
|
state.prog_items[self.player]["state_is_fresh"] = 0
|
|
|
|
return change
|
|
|
|
def remove(self, state: CollectionState, item: Item) -> bool:
|
|
change = super().remove(state, item)
|
|
if change:
|
|
state.prog_items[self.player]["state_is_fresh"] = 0
|
|
|
|
return change
|