This commit is contained in:
spinerak
2024-06-07 23:16:16 +02:00
parent 3d30b3b8e5
commit 064c92977a
6 changed files with 185 additions and 167 deletions

View File

@@ -8,10 +8,10 @@ class ItemData(typing.NamedTuple):
class YachtDiceItem(Item): class YachtDiceItem(Item):
game: str = "Yacht Dice" game: str = "Yacht Dice"
#the starting index is chosen semi-randomly to be 16871244000 # the starting index is chosen semi-randomly to be 16871244000
item_table = { item_table = {
#victory item, always placed manually at goal location # victory item, always placed manually at goal location
"Victory": ItemData(16871244000-1, ItemClassification.progression), "Victory": ItemData(16871244000-1, ItemClassification.progression),
"Dice": ItemData(16871244000, ItemClassification.progression), "Dice": ItemData(16871244000, ItemClassification.progression),
@@ -19,7 +19,6 @@ item_table = {
"Roll": ItemData(16871244002, ItemClassification.progression), "Roll": ItemData(16871244002, ItemClassification.progression),
"Roll Fragment": ItemData(16871244003, ItemClassification.progression), "Roll Fragment": ItemData(16871244003, ItemClassification.progression),
#"Score Multiplier": ItemData(16871244004, ItemClassification.progression), #not used anymore
"Fixed Score Multiplier": ItemData(16871244005, ItemClassification.progression), "Fixed Score Multiplier": ItemData(16871244005, ItemClassification.progression),
"Step Score Multiplier": ItemData(16871244006, ItemClassification.progression), "Step Score Multiplier": ItemData(16871244006, ItemClassification.progression),
@@ -57,21 +56,21 @@ item_table = {
"Category Five Distinct Dice": ItemData(16871244137, ItemClassification.progression), "Category Five Distinct Dice": ItemData(16871244137, ItemClassification.progression),
"Category 4&5 Full House": ItemData(16871244138, ItemClassification.progression), "Category 4&5 Full House": ItemData(16871244138, ItemClassification.progression),
#filler items # filler items
"Encouragement": ItemData(16871244200, ItemClassification.filler), "Encouragement": ItemData(16871244200, ItemClassification.filler),
"Fun Fact": ItemData(16871244201, ItemClassification.filler), "Fun Fact": ItemData(16871244201, ItemClassification.filler),
"Story Chapter": ItemData(16871244202, ItemClassification.filler), "Story Chapter": ItemData(16871244202, ItemClassification.filler),
"Good RNG": ItemData(16871244203, ItemClassification.filler), "Good RNG": ItemData(16871244203, ItemClassification.filler),
"Bad RNG": ItemData(16871244204, ItemClassification.trap), "Bad RNG": ItemData(16871244204, ItemClassification.trap),
"Bonus Point": ItemData(16871244205, ItemClassification.useful), #not included in logic "Bonus Point": ItemData(16871244205, ItemClassification.useful), # not included in logic
#These points are included in the logic and might be necessary to progress. # These points are included in the logic and might be necessary to progress.
"1 Point": ItemData(16871244301, ItemClassification.progression_skip_balancing), "1 Point": ItemData(16871244301, ItemClassification.progression_skip_balancing),
"10 Points": ItemData(16871244302, ItemClassification.progression), "10 Points": ItemData(16871244302, ItemClassification.progression),
"100 Points": ItemData(16871244303, ItemClassification.progression) "100 Points": ItemData(16871244303, ItemClassification.progression)
} }
#item groups for better hinting # item groups for better hinting
item_groups = { item_groups = {
"Score Multiplier": { "Score Multiplier": {
"Step Score Multiplier", "Step Score Multiplier",

View File

@@ -14,31 +14,31 @@ class YachtDiceLocation(Location):
self.yacht_dice_score = score self.yacht_dice_score = score
all_locations = {} all_locations = {}
starting_index = 16871244500 #500 more than the starting index for items starting_index = 16871244500 # 500 more than the starting index for items
#Function that is called when this file is loaded, which loads in ALL possible locations, score 1 to 1000 # Function that is called when this file is loaded, which loads in ALL possible locations, score 1 to 1000
def all_locations_fun(max_score): def all_locations_fun(max_score):
location_table = {} location_table = {}
for i in range(max_score+1): for i in range(max_score+1):
location_table[f"{i} score"] = LocData(starting_index+i, "Board", i) location_table[f"{i} score"] = LocData(starting_index+i, "Board", i)
return location_table return location_table
#function that loads in all locations necessary for the game, so based on options. # function that loads in all locations necessary for the game, so based on options.
#will make sure that goal_score and max_score are included locations # will make sure that goal_score and max_score are included locations
def ini_locations(goal_score, max_score, num_locs, dif): def ini_locations(goal_score, max_score, num_locs, dif):
scaling = 2 #parameter that determines how many low-score location there are. scaling = 2 # parameter that determines how many low-score location there are.
#need more low-score locations or lower difficulties: # need more low-score locations or lower difficulties:
if dif == 1: if dif == 1:
scaling = 3 scaling = 3
elif dif == 2: elif dif == 2:
scaling = 2.3 scaling = 2.3
scores = [] scores = []
#the scores follow the function int( 1 + (perc ** scaling) * (max_score-1) ) # the scores follow the function int( 1 + (perc ** scaling) * (max_score-1) )
#however, this will have many low values, sometimes repeating. # however, this will have many low values, sometimes repeating.
#to avoid repeating scores, hiscore keeps tracks of the highest score location # to avoid repeating scores, hiscore keeps tracks of the highest score location
#and the next score will always be at least hiscore + 1 # and the next score will always be at least hiscore + 1
#note that curscore is at most max_score-1 # note that curscore is at most max_score-1
hiscore = 0 hiscore = 0
for i in range(num_locs - 1): for i in range(num_locs - 1):
perc = (i/num_locs) perc = (i/num_locs)
@@ -49,7 +49,7 @@ def ini_locations(goal_score, max_score, num_locs, dif):
scores += [curscore] scores += [curscore]
if goal_score != max_score: if goal_score != max_score:
#if the goal score is not in the list, find the closest one and make it the goal. # if the goal score is not in the list, find the closest one and make it the goal.
if goal_score not in scores: if goal_score not in scores:
closest_num = min(scores, key=lambda x: abs(x - 500)) closest_num = min(scores, key=lambda x: abs(x - 500))
scores[scores.index(closest_num)] = goal_score scores[scores.index(closest_num)] = goal_score

View File

@@ -15,8 +15,9 @@ class GameDifficulty(Choice):
option_hard = 3 option_hard = 3
option_extreme = 4 option_extreme = 4
default = 2 default = 2
class scoreForLastCheck(Range): class ScoreForLastCheck(Range):
""" """
The items in the item pool will always allow you to reach a score of 1000. The items in the item pool will always allow you to reach a score of 1000.
By default, the last check is also at a score of 1000. By default, the last check is also at a score of 1000.
@@ -26,8 +27,9 @@ class scoreForLastCheck(Range):
range_start = 500 range_start = 500
range_end = 1000 range_end = 1000
default = 1000 default = 1000
class scoreForGoal(Range):
class ScoreForGoal(Range):
""" """
This option determines what score you need to reach to finish the game. This option determines what score you need to reach to finish the game.
It cannot be higher than the score for the last check (if it is, it is changed automatically). It cannot be higher than the score for the last check (if it is, it is changed automatically).
@@ -37,7 +39,8 @@ class scoreForGoal(Range):
range_end = 1000 range_end = 1000
default = 777 default = 777
class minimalNumberOfDiceAndRolls(Choice):
class MinimalNumberOfDiceAndRolls(Choice):
""" """
The minimal number of dice and rolls in the pool. The minimal number of dice and rolls in the pool.
These are guaranteed, unlike the later items. These are guaranteed, unlike the later items.
@@ -52,7 +55,8 @@ class minimalNumberOfDiceAndRolls(Choice):
option_8_dice_and_2_rolls = 6 option_8_dice_and_2_rolls = 6
default = 2 default = 2
class numberDiceFragmentsPerDice(Range):
class NumberDiceFragmentsPerDice(Range):
""" """
Dice can be split into fragments, gathering enough will give you an extra dice. Dice can be split into fragments, gathering enough will give you an extra dice.
You start with one dice, and there will always be one full dice in the pool. You start with one dice, and there will always be one full dice in the pool.
@@ -63,8 +67,9 @@ class numberDiceFragmentsPerDice(Range):
range_start = 1 range_start = 1
range_end = 5 range_end = 5
default = 4 default = 4
class numberRollFragmentsPerRoll(Range):
class NumberRollFragmentsPerRoll(Range):
""" """
Rolls can be split into fragments, gathering enough will give you an extra roll. Rolls can be split into fragments, gathering enough will give you an extra roll.
You start with one roll, and there will always be one full roll in the pool. You start with one roll, and there will always be one full roll in the pool.
@@ -75,9 +80,9 @@ class numberRollFragmentsPerRoll(Range):
range_start = 1 range_start = 1
range_end = 5 range_end = 5
default = 4 default = 4
class alternativeCategories(Range): class AlternativeCategories(Range):
""" """
There are 16 default categories, but there are also 16 alternative categories. There are 16 default categories, but there are also 16 alternative categories.
These alternative categories can be randomly selected to replace the default categories. These alternative categories can be randomly selected to replace the default categories.
@@ -89,9 +94,9 @@ class alternativeCategories(Range):
range_start = 0 range_start = 0
range_end = 16 range_end = 16
default = 0 default = 0
class chanceOfDice(Range): class ChanceOfDice(Range):
""" """
The item pool is always filled in such a way that you can reach a score of 1000. The item pool is always filled in such a way that you can reach a score of 1000.
Extra progression items are added that will help you on your quest. Extra progression items are added that will help you on your quest.
@@ -103,8 +108,9 @@ class chanceOfDice(Range):
range_start = 0 range_start = 0
range_end = 100 range_end = 100
default = 5 default = 5
class chanceOfRoll(Range):
class ChanceOfRoll(Range):
""" """
With more rolls, you will be able to reach higher scores. With more rolls, you will be able to reach higher scores.
""" """
@@ -113,7 +119,8 @@ class chanceOfRoll(Range):
range_end = 100 range_end = 100
default = 20 default = 20
class chanceOfFixedScoreMultiplier(Range):
class ChanceOfFixedScoreMultiplier(Range):
""" """
Getting a Fixed Score Multiplier will boost all future scores by 10%. Getting a Fixed Score Multiplier will boost all future scores by 10%.
""" """
@@ -121,8 +128,9 @@ class chanceOfFixedScoreMultiplier(Range):
range_start = 0 range_start = 0
range_end = 100 range_end = 100
default = 30 default = 30
class chanceOfStepScoreMultiplier(Range):
class ChanceOfStepScoreMultiplier(Range):
""" """
The Step Score Multiplier boosts your multiplier after every roll by 1%, and resets on sheet reset. The Step Score Multiplier boosts your multiplier after every roll by 1%, and resets on sheet reset.
So, keep high scoring categories for later to get the most out of them. So, keep high scoring categories for later to get the most out of them.
@@ -132,8 +140,9 @@ class chanceOfStepScoreMultiplier(Range):
range_start = 0 range_start = 0
range_end = 100 range_end = 100
default = 0 default = 0
class chanceOfDoubleCategory(Range):
class ChanceOfDoubleCategory(Range):
""" """
This option allows categories to appear multiple times. This option allows categories to appear multiple times.
Each time you get a category after the first, its score value gets doubled. Each time you get a category after the first, its score value gets doubled.
@@ -142,8 +151,9 @@ class chanceOfDoubleCategory(Range):
range_start = 0 range_start = 0
range_end = 100 range_end = 100
default = 50 default = 50
class chanceOfPoints(Range):
class ChanceOfPoints(Range):
""" """
Getting points gives you... points. You can get 1 point, 10 points, and even 100 points. Getting points gives you... points. You can get 1 point, 10 points, and even 100 points.
""" """
@@ -151,8 +161,9 @@ class chanceOfPoints(Range):
range_start = 0 range_start = 0
range_end = 100 range_end = 100
default = 20 default = 20
class pointsSize(Choice):
class PointsSize(Choice):
""" """
If you choose to add points to the item pool, do you prefer many small points, If you choose to add points to the item pool, do you prefer many small points,
medium-size points, a few larger points, or a mix of them? medium-size points, a few larger points, or a mix of them?
@@ -163,8 +174,9 @@ class pointsSize(Choice):
option_large = 3 option_large = 3
option_mix = 4 option_mix = 4
default = 2 default = 2
class minimizeExtraItems(Choice):
class MinimizeExtraItems(Choice):
""" """
Besides necessary items, Yacht Dice has extra useful/filler items in the item pool. Besides necessary items, Yacht Dice has extra useful/filler items in the item pool.
It is possible however to decrease the number of extra items in multiplayer games. It is possible however to decrease the number of extra items in multiplayer games.
@@ -175,8 +187,9 @@ class minimizeExtraItems(Choice):
option_no_dont = 1 option_no_dont = 1
option_yes_please = 2 option_yes_please = 2
default = 1 default = 1
class addExtraPoints(Choice):
class AddExtraPoints(Choice):
""" """
Yacht Dice typically has space for extra items. Yacht Dice typically has space for extra items.
If there is space, would you like bonus points shuffled in the item pool? If there is space, would you like bonus points shuffled in the item pool?
@@ -191,8 +204,9 @@ class addExtraPoints(Choice):
option_sure = 2 option_sure = 2
option_never = 3 option_never = 3
default = 2 default = 2
class addStoryChapters(Choice):
class AddStoryChapters(Choice):
""" """
Yacht Dice typically has space for more items. Yacht Dice typically has space for more items.
If there is space, would you like story chapters shuffled in the item pool? If there is space, would you like story chapters shuffled in the item pool?
@@ -207,8 +221,9 @@ class addStoryChapters(Choice):
option_sure = 2 option_sure = 2
option_never = 3 option_never = 3
default = 3 default = 3
class whichStory(Choice):
class WhichStory(Choice):
""" """
The most important part of Yacht Dice is the narrative. The most important part of Yacht Dice is the narrative.
If you choose to If you choose to
@@ -225,8 +240,9 @@ class whichStory(Choice):
option_a_rollin_rhyme_adventure = 6 option_a_rollin_rhyme_adventure = 6
option_random_story = -1 option_random_story = -1
default = -1 default = -1
class allowManual(Choice):
class AllowManual(Choice):
""" """
Yacht Dice allows players to roll IRL dice. Yacht Dice allows players to roll IRL dice.
By sending "manual" in the chat, an input field appears where you can type your dice rolls. By sending "manual" in the chat, an input field appears where you can type your dice rolls.
@@ -237,32 +253,33 @@ class allowManual(Choice):
option_yes_allow = 1 option_yes_allow = 1
option_no_dont_allow = 2 option_no_dont_allow = 2
default = 1 default = 1
@dataclass @dataclass
class YachtDiceOptions(PerGameCommonOptions): class YachtDiceOptions(PerGameCommonOptions):
game_difficulty: gameDifficulty game_difficulty: GameDifficulty
score_for_last_check: scoreForLastCheck score_for_last_check: ScoreForLastCheck
score_for_goal: scoreForGoal score_for_goal: ScoreForGoal
minimal_number_of_dice_and_rolls: minimalNumberOfDiceAndRolls minimal_number_of_dice_and_rolls: MinimalNumberOfDiceAndRolls
number_of_dice_fragments_per_dice: numberDiceFragmentsPerDice number_of_dice_fragments_per_dice: NumberDiceFragmentsPerDice
number_of_roll_fragments_per_roll: numberRollFragmentsPerRoll number_of_roll_fragments_per_roll: NumberRollFragmentsPerRoll
alternative_categories: alternativeCategories alternative_categories: AlternativeCategories
#the following options determine what extra items are shuffled into the pool: #the following options determine what extra items are shuffled into the pool:
weight_of_dice: chanceOfDice weight_of_dice: ChanceOfDice
weight_of_roll: chanceOfRoll weight_of_roll: ChanceOfRoll
weight_of_fixed_score_multiplier: chanceOfFixedScoreMultiplier weight_of_fixed_score_multiplier: ChanceOfFixedScoreMultiplier
weight_of_step_score_multiplier: chanceOfStepScoreMultiplier weight_of_step_score_multiplier: ChanceOfStepScoreMultiplier
weight_of_double_category: chanceOfDoubleCategory weight_of_double_category: ChanceOfDoubleCategory
weight_of_points: chanceOfPoints weight_of_points: ChanceOfPoints
points_size: pointsSize points_size: PointsSize
minimize_extra_items: minimizeExtraItems minimize_extra_items: MinimizeExtraItems
add_bonus_points: addExtraPoints add_bonus_points: AddExtraPoints
add_story_chapters: addStoryChapters add_story_chapters: AddStoryChapters
which_story: whichStory which_story: WhichStory
allow_manual_input: allowManual allow_manual_input: AllowManual

View File

@@ -4,7 +4,7 @@ from .YachtWeights import yacht_weights
import math import math
from collections import defaultdict from collections import defaultdict
#List of categories, and the name of the logic class associated with it # List of categories, and the name of the logic class associated with it
category_mappings = { category_mappings = {
"Category Ones": "Ones", "Category Ones": "Ones",
"Category Twos": "Twos", "Category Twos": "Twos",
@@ -41,21 +41,21 @@ category_mappings = {
"Category 4&5 Full House": "FourAndFiveFullHouse" "Category 4&5 Full House": "FourAndFiveFullHouse"
} }
#This class adds logic to the apworld. # This class adds logic to the apworld.
#In short, we ran a simulation for every possible combination of dice and rolls you can have, per category. # In short, we ran a simulation for every possible combination of dice and rolls you can have, per category.
#This simulation has a good strategy for locking dice. # This simulation has a good strategy for locking dice.
#This gives rise to an approximate discrete distribution per category. # This gives rise to an approximate discrete distribution per category.
#We calculate the distribution of the total score. # We calculate the distribution of the total score.
#We then pick a correct percentile to reflect the correct score that should be in logic. # We then pick a correct percentile to reflect the correct score that should be in logic.
#The score is logic is *much* lower than the actual maximum reachable score. # The score is logic is *much* lower than the actual maximum reachable score.
class Category: class Category:
def __init__(self, name, quantity = 1): def __init__(self, name, quantity = 1):
self.name = name self.name = name
self.quantity = quantity #how many times you have the category self.quantity = quantity # how many times you have the category
#return mean score of a category # return mean score of a category
def mean_score(self, num_dice, num_rolls): def mean_score(self, num_dice, num_rolls):
if num_dice == 0 or num_rolls == 0: if num_dice == 0 or num_rolls == 0:
return 0 return 0
@@ -66,10 +66,10 @@ class Category:
def extract_progression(state, player, options): def extract_progression(state, player, options):
#method to obtain a list of what items the player has. # method to obtain a list of what items the player has.
#this includes categories, dice, rolls and score multiplier etc. # this includes categories, dice, rolls and score multiplier etc.
if player == "state_is_a_list": #the state variable is just a list with the names of the items if player == "state_is_a_list": # the state variable is just a list with the names of the items
number_of_dice = ( number_of_dice = (
state.count("Dice") state.count("Dice")
+ state.count("Dice Fragment") // options.number_of_dice_fragments_per_dice.value + state.count("Dice Fragment") // options.number_of_dice_fragments_per_dice.value
@@ -89,7 +89,7 @@ def extract_progression(state, player, options):
extra_points_in_logic = state.count("1 Point") extra_points_in_logic = state.count("1 Point")
extra_points_in_logic += state.count("10 Points") * 10 extra_points_in_logic += state.count("10 Points") * 10
extra_points_in_logic += state.count("100 Points") * 100 extra_points_in_logic += state.count("100 Points") * 100
else: #state is an Archipelago object, so we need state.count(..., player) else: # state is an Archipelago object, so we need state.count(..., player)
number_of_dice = ( number_of_dice = (
state.count("Dice", player) state.count("Dice", player)
+ state.count("Dice Fragment", player) // options.number_of_dice_fragments_per_dice.value + state.count("Dice Fragment", player) // options.number_of_dice_fragments_per_dice.value
@@ -113,23 +113,23 @@ def extract_progression(state, player, options):
return [categories, number_of_dice, number_of_rerolls, return [categories, number_of_dice, number_of_rerolls,
number_of_fixed_mults * 0.1, number_of_step_mults * 0.01, extra_points_in_logic] number_of_fixed_mults * 0.1, number_of_step_mults * 0.01, extra_points_in_logic]
#We will store the results of this function as it is called often for the same parameters. # We will store the results of this function as it is called often for the same parameters.
yachtdice_cache = {} yachtdice_cache = {}
#Function that returns the feasible score in logic based on items obtained. # Function that returns the feasible score in logic based on items obtained.
def dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mult, diff): def dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mult, diff):
tup = tuple([tuple(sorted([c.name+str(c.quantity) for c in categories])), tup = tuple([tuple(sorted([c.name+str(c.quantity) for c in categories])),
num_dice, num_rolls, fixed_mult, step_mult, diff]) #identifier num_dice, num_rolls, fixed_mult, step_mult, diff]) #identifier
#if already computed, return the result # if already computed, return the result
if tup in yachtdice_cache.keys(): if tup in yachtdice_cache.keys():
return yachtdice_cache[tup] return yachtdice_cache[tup]
#sort categories because for the step multiplier, you will want low-scoring categories first # sort categories because for the step multiplier, you will want low-scoring categories first
categories.sort(key=lambda category: category.mean_score(num_dice, num_rolls)) categories.sort(key=lambda category: category.mean_score(num_dice, num_rolls))
#function to add two discrete distribution. # function to add two discrete distribution.
#defaultdict is a dict where you don't need to check if an id is present, you can just use += (lot faster) # defaultdict is a dict where you don't need to check if an id is present, you can just use += (lot faster)
def add_distributions(dist1, dist2): def add_distributions(dist1, dist2):
combined_dist = defaultdict(float) combined_dist = defaultdict(float)
for val1, prob1 in dist1.items(): for val1, prob1 in dist1.items():
@@ -137,8 +137,8 @@ def dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mu
combined_dist[val1 + val2] += prob1 * prob2 combined_dist[val1 + val2] += prob1 * prob2
return dict(combined_dist) return dict(combined_dist)
#function to take the maximum of "times" i.i.d. dist1. # function to take the maximum of "times" i.i.d. dist1.
#(I have tried using defaultdict here too but this made it slower.) # (I have tried using defaultdict here too but this made it slower.)
def max_dist(dist1, mults): def max_dist(dist1, mults):
new_dist = {0: 1} new_dist = {0: 1}
for mult in mults: for mult in mults:
@@ -157,7 +157,7 @@ def dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mu
return new_dist return new_dist
#Returns percentile value of a distribution. # Returns percentile value of a distribution.
def percentile_distribution(dist, percentile): def percentile_distribution(dist, percentile):
sorted_values = sorted(dist.keys()) sorted_values = sorted(dist.keys())
cumulative_prob = 0 cumulative_prob = 0
@@ -172,13 +172,13 @@ def dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mu
# Return the first value if percentile is lower than all probabilities # Return the first value if percentile is lower than all probabilities
return prev_val if prev_val is not None else sorted_values[0] return prev_val if prev_val is not None else sorted_values[0]
#parameters for logic. # parameters for logic.
#perc_return is, per difficulty, the percentages of total score it returns (it averages out the values) # perc_return is, per difficulty, the percentages of total score it returns (it averages out the values)
#diff_divide determines how many shots the logic gets per category. Lower = more shots. # diff_divide determines how many shots the logic gets per category. Lower = more shots.
perc_return = [[0], [0.1, 0.5], [0.3, 0.7], [0.55, 0.85], [0.85, 0.95]][diff] perc_return = [[0], [0.1, 0.5], [0.3, 0.7], [0.55, 0.85], [0.85, 0.95]][diff]
diff_divide = [0, 9, 7, 3, 2][diff] diff_divide = [0, 9, 7, 3, 2][diff]
#calculate total distribution # calculate total distribution
total_dist = {0: 1} total_dist = {0: 1}
for j in range(len(categories)): for j in range(len(categories)):
if num_dice == 0 or num_rolls == 0: if num_dice == 0 or num_rolls == 0:
@@ -191,14 +191,14 @@ def dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mu
cat_mult = 2 ** (categories[j].quantity-1) cat_mult = 2 ** (categories[j].quantity-1)
#for higher difficulties, the simulation gets multiple tries for categories. # for higher difficulties, the simulation gets multiple tries for categories.
max_tries = j // diff_divide max_tries = j // diff_divide
mults = [(1 + fixed_mult + step_mult * ii) * cat_mult for ii in range(max(0,j - max_tries), j+1)] mults = [(1 + fixed_mult + step_mult * ii) * cat_mult for ii in range(max(0,j - max_tries), j+1)]
dist = max_dist(dist, mults) dist = max_dist(dist, mults)
total_dist = add_distributions(total_dist, dist) total_dist = add_distributions(total_dist, dist)
#save result into the cache, then return it # save result into the cache, then return it
outcome = sum([percentile_distribution(total_dist, perc) for perc in perc_return]) / len(perc_return) outcome = sum([percentile_distribution(total_dist, perc) for perc in perc_return]) / len(perc_return)
yachtdice_cache[tup] = max(5, math.floor(outcome)) yachtdice_cache[tup] = max(5, math.floor(outcome))
return yachtdice_cache[tup] return yachtdice_cache[tup]

File diff suppressed because one or more lines are too long

View File

@@ -56,17 +56,17 @@ class YachtDiceWorld(World):
self.itempool = [] self.itempool = []
self.precollected = [] self.precollected = []
#number of dice and rolls in the pull # number of dice and rolls in the pull
ind_dice_rolls = self.options.minimal_number_of_dice_and_rolls.value 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_dice = [0, 2, 5, 5, 6, 7, 8][ind_dice_rolls]
num_of_rolls = [0, 2, 3, 5, 4, 3, 2][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 # 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_dice = self.options.number_of_dice_fragments_per_dice.value
frags_per_roll = self.options.number_of_roll_fragments_per_roll.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 # count number of plando items not from pool, we need extra locations for them
self.extra_plando_items = 0 self.extra_plando_items = 0
for plando_setting in self.multiworld.plando_items[self.player]: for plando_setting in self.multiworld.plando_items[self.player]:
@@ -80,9 +80,9 @@ class YachtDiceWorld(World):
# Shuffle the list to randomize the order # Shuffle the list to randomize the order
self.multiworld.random.shuffle(categorylist) self.multiworld.random.shuffle(categorylist)
#A list of all possible categories. # A list of all possible categories.
#Every entry in the list has two categories, one 'default' category and one 'alt'. # 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. # You get either of the two for every entry, so a total of 16 unique categories.
all_categories = [ all_categories = [
["Category Choice", "Category Double Threes and Fours"], ["Category Choice", "Category Double Threes and Fours"],
["Category Inverse Choice", "Category Quadruple Ones and Twos"], ["Category Inverse Choice", "Category Quadruple Ones and Twos"],
@@ -102,7 +102,7 @@ class YachtDiceWorld(World):
["Category Yacht", "Category 4&5 Full House"] ["Category Yacht", "Category 4&5 Full House"]
] ]
#categories used in this game. # categories used in this game.
possible_categories = [] possible_categories = []
for index, cats in enumerate(all_categories): for index, cats in enumerate(all_categories):
@@ -118,37 +118,37 @@ class YachtDiceWorld(World):
self.precollected += ["Roll"] self.precollected += ["Roll"]
self.precollected += ["Dice"] self.precollected += ["Dice"]
#if one fragment per dice, just add "Dice" objects # if one fragment per dice, just add "Dice" objects
if frags_per_dice == 1: if frags_per_dice == 1:
self.itempool += ["Dice"] * (num_of_dice-1) # minus one because one is in start inventory self.itempool += ["Dice"] * (num_of_dice-1) # minus one because one is in start inventory
else: else:
self.itempool += ["Dice"] #always add a full dice to make generation easier (will be early) 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)) self.itempool += ["Dice Fragment"] * (frags_per_dice * (num_of_dice-2))
#if one fragment per roll, just add "Roll" objects # if one fragment per roll, just add "Roll" objects
if frags_per_roll == 1: if frags_per_roll == 1:
self.itempool += ["Roll"] * (num_of_rolls-1) #minus one because one is in start inventory self.itempool += ["Roll"] * (num_of_rolls-1) # minus one because one is in start inventory
else: else:
self.itempool += ["Roll"] #always add a full roll to make generation easier (will be early) 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)) self.itempool += ["Roll Fragment"] * (frags_per_roll * (num_of_rolls-2))
already_items = len(self.itempool) + self.extra_plando_items already_items = len(self.itempool) + self.extra_plando_items
#Yacht Dice needs extra filler items so it doesn't get stuck in generation. # 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. # For now, we calculate the number of extra items we'll need later.
if self.options.minimize_extra_items.value: if self.options.minimize_extra_items.value:
extraPercentage = max(0.1, 0.8 - self.multiworld.players / 10) extraPercentage = max(0.1, 0.8 - self.multiworld.players / 10)
else: else:
extraPercentage = 0.7 extraPercentage = 0.7
extra_locations_needed = max(10, math.ceil(already_items * extraPercentage)) 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 # 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.max_score = self.options.score_for_last_check.value
self.goal_score = min(self.max_score, self.options.score_for_goal.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. # 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. # 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. # If all weights are 0, some of them will be made to be non-zero later.
weights = [ weights = [
self.options.weight_of_dice.value, self.options.weight_of_dice.value,
self.options.weight_of_roll.value, self.options.weight_of_roll.value,
@@ -158,39 +158,39 @@ class YachtDiceWorld(World):
self.options.weight_of_points.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 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: if weights[0] > 0 and frags_per_dice > 1:
self.itempool += ["Dice Fragment"] * (frags_per_dice - 1) self.itempool += ["Dice Fragment"] * (frags_per_dice - 1)
if weights[1] > 0 and frags_per_roll > 1: if weights[1] > 0 and frags_per_roll > 1:
self.itempool += ["Roll Fragment"] * (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 # calibrate the weights, since the impact of each of the items is different
weights[0] = weights[0] / 5 * frags_per_dice weights[0] = weights[0] / 5 * frags_per_dice
weights[1] = weights[1] / 5 * frags_per_roll weights[1] = weights[1] / 5 * frags_per_roll
extra_points_added = 0 extra_points_added = 0
multipliers_added = 0 multipliers_added = 0
#Keep adding items until a score of 1000 is in logic # 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: while dice_simulation(self.itempool + self.precollected, "state_is_a_list", self.options) < 1000:
all_items = self.itempool + self.precollected all_items = self.itempool + self.precollected
dice_fragments_in_pool = all_items.count("Dice") * frags_per_dice + all_items.count("Dice Fragment") 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: if dice_fragments_in_pool + 1 >= 9 * frags_per_dice:
weights[0] = 0 #don't allow >=9 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") 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: if roll_fragments_in_pool + 1 >= 6 * frags_per_roll:
weights[1] = 0 # don't allow >= 6 rolls weights[1] = 0 # don't allow >= 6 rolls
#Don't allow too many multipliers # Don't allow too many multipliers
if multipliers_added > 50: if multipliers_added > 50:
weights[2] = 0 weights[2] = 0
weights[3] = 0 weights[3] = 0
#Don't allow too many extra points # Don't allow too many extra points
if extra_points_added > 300: if extra_points_added > 300:
weights[5] = 0 weights[5] = 0
#if all weights are zero, allow to add fixed score multiplier, double category, points. # if all weights are zero, allow to add fixed score multiplier, double category, points.
if sum(weights) == 0: if sum(weights) == 0:
if multipliers_added <= 50: if multipliers_added <= 50:
weights[2] = 1 weights[2] = 1
@@ -198,7 +198,7 @@ class YachtDiceWorld(World):
if extra_points_added <= 300: if extra_points_added <= 300:
weights[5] = 1 weights[5] = 1
#Next, add the appropriate item. We'll slightly alter weights to avoid too many of the same item # 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] which_item_to_add = self.multiworld.random.choices([0, 1, 2, 3, 4, 5], weights = weights)[0]
if which_item_to_add == 0: if which_item_to_add == 0:
if frags_per_dice == 1: if frags_per_dice == 1:
@@ -226,16 +226,16 @@ class YachtDiceWorld(World):
weights[4] /= 1.1 weights[4] /= 1.1
elif which_item_to_add == 5: elif which_item_to_add == 5:
score_dist = self.options.points_size.value score_dist = self.options.points_size.value
probs = [1,0,0] probs = [1, 0, 0]
if score_dist == 1: if score_dist == 1:
probs = [0.9,0.08,0] probs = [0.9, 0.08, 0]
if score_dist == 2: if score_dist == 2:
probs = [0,1,0] probs = [0, 1, 0]
if score_dist == 3: if score_dist == 3:
probs = [0,0.3,0.7] probs = [0, 0.3, 0.7]
if score_dist == 4: if score_dist == 4:
probs = [0.3,0.4,0.3] probs = [0.3, 0.4, 0.3]
c = self.multiworld.random.choices([0,1,2], weights = probs)[0] c = self.multiworld.random.choices([0, 1, 2], weights = probs)[0]
if c == 0: if c == 0:
self.itempool += ["1 Point"] self.itempool += ["1 Point"]
extra_points_added += 1 extra_points_added += 1
@@ -253,56 +253,56 @@ class YachtDiceWorld(World):
else: else:
raise Exception("Invalid index when adding new items in Yacht Dice") raise Exception("Invalid index when adding new items in Yacht Dice")
#count the number of locations in the game. # count the number of locations in the game.
already_items = len(self.itempool) + self.extra_plando_items + 1 #+1 because of Victory item 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 successful generation # We need to add more filler/useful items if there are many items in the pool to guarantee successful generation
extra_locations_needed += (already_items - 45) // 15 extra_locations_needed += (already_items - 45) // 15
self.number_of_locations = already_items + extra_locations_needed self.number_of_locations = already_items + extra_locations_needed
# From here, we will count the number of items in the self.itempool, and add useful/filler items to the pool, # From here, we will count the number of items in the self.itempool, and add useful/filler items to the pool,
# making sure not to exceed the number of locations. # making sure not to exceed the number of locations.
#first, we flood the entire pool with extra points (useful), if that setting is chosen. # 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 if self.options.add_bonus_points.value == 1: #all of the extra points
already_items = len(self.itempool) + self.extra_plando_items + 1 already_items = len(self.itempool) + self.extra_plando_items + 1
self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 100) 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. # 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 if self.options.add_story_chapters.value == 1: # all of the story chapters
already_items = len(self.itempool) + self.extra_plando_items + 1 already_items = len(self.itempool) + self.extra_plando_items + 1
number_of_items = min(self.number_of_locations - already_items, 100) 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 number_of_items = (number_of_items // 10) * 10 # story chapters always come in multiples of 10
self.itempool += ["Story Chapter"] * number_of_items self.itempool += ["Story Chapter"] * number_of_items
#add some extra points (useful) # add some extra points (useful)
if self.options.add_bonus_points.value == 2: #add extra points if wanted if self.options.add_bonus_points.value == 2: # add extra points if wanted
already_items = len(self.itempool) + self.extra_plando_items + 1 already_items = len(self.itempool) + self.extra_plando_items + 1
self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 10) self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 10)
#add some story chapters (filler) # add some story chapters (filler)
if self.options.add_story_chapters.value == 2: #add extra points if wanted if self.options.add_story_chapters.value == 2: # add extra points if wanted
already_items = len(self.itempool) + self.extra_plando_items + 1 already_items = len(self.itempool) + self.extra_plando_items + 1
if self.number_of_locations - already_items >= 10: if self.number_of_locations - already_items >= 10:
self.itempool += ["Story Chapter"] * 10 self.itempool += ["Story Chapter"] * 10
#add some more extra points if there is still room # add some more extra points if there is still room
if self.options.add_bonus_points.value == 2: if self.options.add_bonus_points.value == 2:
already_items = len(self.itempool) + self.extra_plando_items + 1 already_items = len(self.itempool) + self.extra_plando_items + 1
self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 10) self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 10)
#add some encouragements filler-items if there is still room # add some encouragements filler-items if there is still room
already_items = len(self.itempool) + self.extra_plando_items + 1 already_items = len(self.itempool) + self.extra_plando_items + 1
self.itempool += ["Encouragement"] * min(self.number_of_locations - already_items, 5) self.itempool += ["Encouragement"] * min(self.number_of_locations - already_items, 5)
#add some fun facts filler-items if there is still room # add some fun facts filler-items if there is still room
already_items = len(self.itempool) + self.extra_plando_items + 1 already_items = len(self.itempool) + self.extra_plando_items + 1
self.itempool += ["Fun Fact"] * min(self.number_of_locations - already_items, 5) 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 # finally, add some "Good RNG" and "Bad RNG" items to complete the item pool
#these items are filler and do not do anything. # these items are filler and do not do anything.
#probability of Good and Bad rng, based on difficulty for fun :) # probability of Good and Bad rng, based on difficulty for fun :)
p = 1.1 - 0.25 * self.options.game_difficulty.value p = 1.1 - 0.25 * self.options.game_difficulty.value
already_items = len(self.itempool) + self.extra_plando_items + 1 already_items = len(self.itempool) + self.extra_plando_items + 1
self.itempool += self.multiworld.random.choices( self.itempool += self.multiworld.random.choices(
@@ -311,17 +311,17 @@ class YachtDiceWorld(World):
k=self.number_of_locations - already_items 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 # 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 already_items = len(self.itempool) + self.extra_plando_items + 1
if already_items != self.number_of_locations: if already_items != self.number_of_locations:
raise Exception(f"[Yacht Dice] Number in self.itempool is not number of locations " raise Exception(f"[Yacht Dice] Number in self.itempool is not number of locations "
f"{already_items} {self.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 # add precollected items using push_precollected. Items in self.itempool get created in create_items
for item in self.precollected: for item in self.precollected:
self.multiworld.push_precollected(self.create_item(item)) 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 # 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]["Dice"] = 1
self.multiworld.early_items[self.player]["Roll"] = 1 self.multiworld.early_items[self.player]["Roll"] = 1
@@ -329,41 +329,41 @@ class YachtDiceWorld(World):
self.multiworld.itempool += [self.create_item(name) for name in self.itempool] self.multiworld.itempool += [self.create_item(name) for name in self.itempool]
def create_regions(self): def create_regions(self):
#call the ini_locations function, that generates locations based on the inputs. # 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, location_table, goal_index = ini_locations(self.goal_score, self.max_score, self.number_of_locations,
self.options.game_difficulty.value) self.options.game_difficulty.value)
#simple menu-board construction # simple menu-board construction
menu = Region("Menu", self.player, self.multiworld) menu = Region("Menu", self.player, self.multiworld)
board = Region("Board", self.player, self.multiworld) board = Region("Board", self.player, self.multiworld)
#add locations to board, one for every location in the location_table # 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) 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] 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. # which index of all locations should have the Victory item.
# Add the victory item to the correct location. # Add the victory item to the correct location.
# The website declares that the game is complete when the victory item is obtained. # 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")) board.locations[goal_index].place_locked_item(self.create_item("Victory"))
#these will be slot_data input # these will be slot_data input
self.goal_score = board.locations[goal_index].yacht_dice_score self.goal_score = board.locations[goal_index].yacht_dice_score
self.max_score = board.locations[-1].yacht_dice_score self.max_score = board.locations[-1].yacht_dice_score
#add the regions # add the regions
connection = Entrance(self.player, "New Board", menu) connection = Entrance(self.player, "New Board", menu)
menu.exits.append(connection) menu.exits.append(connection)
connection.connect(board) connection.connect(board)
self.multiworld.regions += [menu, board] self.multiworld.regions += [menu, board]
def set_rules(self): def set_rules(self):
#set rules per location, and add the rule for beating the game # set rules per location, and add the rule for beating the game
set_yacht_rules(self.multiworld, self.player, self.options) set_yacht_rules(self.multiworld, self.player, self.options)
set_yacht_completion_rules(self.multiworld, self.player) set_yacht_completion_rules(self.multiworld, self.player)
def fill_slot_data(self): def fill_slot_data(self):
#make slot data, which consists of yachtdice_data, options, and some other variables. # make slot data, which consists of yachtdice_data, options, and some other variables.
yacht_dice_data = self._get_yachtdice_data() yacht_dice_data = self._get_yachtdice_data()
yacht_dice_options = self.options.as_dict( yacht_dice_options = self.options.as_dict(
"game_difficulty", "game_difficulty",
@@ -386,7 +386,7 @@ class YachtDiceWorld(World):
"which_story", "which_story",
"allow_manual_input" "allow_manual_input"
) )
slot_data = {**yacht_dice_data, **yacht_dice_options} #combine the two slot_data = {**yacht_dice_data, **yacht_dice_options} # combine the two
slot_data["goal_score"] = self.goal_score slot_data["goal_score"] = self.goal_score
slot_data["last_check_score"] = self.max_score slot_data["last_check_score"] = self.max_score
slot_data["ap_world_version"] = self.ap_world_version slot_data["ap_world_version"] = self.ap_world_version
@@ -397,6 +397,7 @@ class YachtDiceWorld(World):
item = YachtDiceItem(name, item_data.classification, item_data.code, self.player) item = YachtDiceItem(name, item_data.classification, item_data.code, self.player)
return item return item
# We overwrite these function to monitor when states have changed. See also dice_simulation in Rules.py
def collect(self, state: CollectionState, item: Item) -> bool: def collect(self, state: CollectionState, item: Item) -> bool:
change = super().collect(state, item) change = super().collect(state, item)
if change: if change: