mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-25 19:53:23 -07:00
Add the yacht dice (from other git) world to the yacht dice fork
This commit is contained in:
68
worlds/yachtdice/Items.py
Normal file
68
worlds/yachtdice/Items.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from BaseClasses import Item, ItemClassification
|
||||
import typing
|
||||
|
||||
class ItemData(typing.NamedTuple):
|
||||
code: typing.Optional[int]
|
||||
classification: ItemClassification
|
||||
|
||||
class YachtDiceItem(Item):
|
||||
game: str = "Yacht Dice"
|
||||
|
||||
#the starting index is chosen semi-randomly to be 16871244000
|
||||
|
||||
item_table = {
|
||||
"Victory": ItemData(16871244000-1, ItemClassification.progression),
|
||||
|
||||
"Dice": ItemData(16871244000, ItemClassification.progression),
|
||||
"Dice Fragment": ItemData(16871244001, ItemClassification.progression),
|
||||
"Roll": ItemData(16871244002, ItemClassification.progression),
|
||||
"Roll Fragment": ItemData(16871244003, ItemClassification.progression),
|
||||
"Score Multiplier": ItemData(16871244004, ItemClassification.progression),
|
||||
|
||||
"Category Ones": ItemData(16871244103, ItemClassification.progression),
|
||||
"Category Twos": ItemData(16871244104, ItemClassification.progression),
|
||||
"Category Threes": ItemData(16871244105, ItemClassification.progression),
|
||||
"Category Fours": ItemData(16871244106, ItemClassification.progression),
|
||||
"Category Fives": ItemData(16871244107, ItemClassification.progression),
|
||||
"Category Sixes": ItemData(16871244108, ItemClassification.progression),
|
||||
"Category Choice": ItemData(16871244109, ItemClassification.progression),
|
||||
"Category Inverse Choice": ItemData(16871244110, ItemClassification.progression),
|
||||
"Category Pair": ItemData(16871244111, ItemClassification.progression),
|
||||
"Category Three of a Kind": ItemData(16871244112, ItemClassification.progression),
|
||||
"Category Four of a Kind": ItemData(16871244113, ItemClassification.progression),
|
||||
"Category Tiny Straight": ItemData(16871244114, ItemClassification.progression),
|
||||
"Category Small Straight": ItemData(16871244115, ItemClassification.progression),
|
||||
"Category Large Straight": ItemData(16871244116, ItemClassification.progression),
|
||||
"Category Full House": ItemData(16871244117, ItemClassification.progression),
|
||||
"Category Yacht": ItemData(16871244118, ItemClassification.progression),
|
||||
|
||||
"Category Distincts": ItemData(16871244123, ItemClassification.progression),
|
||||
"Category Two times Ones": ItemData(16871244124, ItemClassification.progression),
|
||||
"Category Half of Sixes": ItemData(16871244125, ItemClassification.progression),
|
||||
"Category Twos and Threes": ItemData(16871244126, ItemClassification.progression),
|
||||
"Category Sum of Odds": ItemData(16871244127, ItemClassification.progression),
|
||||
"Category Sum of Evens": ItemData(16871244128, ItemClassification.progression),
|
||||
"Category Double Threes and Fours": ItemData(16871244129, ItemClassification.progression),
|
||||
"Category Quadruple Ones and Twos": ItemData(16871244130, ItemClassification.progression),
|
||||
"Category Micro Straight": ItemData(16871244131, ItemClassification.progression),
|
||||
"Category Three Odds": ItemData(16871244132, ItemClassification.progression),
|
||||
"Category 1-2-1 Consecutive": ItemData(16871244133, ItemClassification.progression),
|
||||
"Category Three Distinct Dice": ItemData(16871244134, ItemClassification.progression),
|
||||
"Category Two Pair": ItemData(16871244135, ItemClassification.progression),
|
||||
"Category 2-1-2 Consecutive": ItemData(16871244136, ItemClassification.progression),
|
||||
"Category Five Distinct Dice": ItemData(16871244137, ItemClassification.progression),
|
||||
"Category 4&5 Full House": ItemData(16871244138, ItemClassification.progression),
|
||||
|
||||
|
||||
|
||||
"Encouragement": ItemData(16871244200, ItemClassification.filler),
|
||||
"Fun Fact": ItemData(16871244201, ItemClassification.filler),
|
||||
"Story Chapter": ItemData(16871244202, ItemClassification.filler),
|
||||
"Good RNG": ItemData(16871244203, ItemClassification.filler),
|
||||
"Bad RNG": ItemData(16871244204, ItemClassification.trap),
|
||||
"Extra Point": ItemData(16871244205, ItemClassification.useful),
|
||||
|
||||
"1 Point": ItemData(16871244301, ItemClassification.progression_skip_balancing),
|
||||
"10 Points": ItemData(16871244302, ItemClassification.progression),
|
||||
"100 Points": ItemData(16871244303, ItemClassification.progression)
|
||||
}
|
||||
62
worlds/yachtdice/Locations.py
Normal file
62
worlds/yachtdice/Locations.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from BaseClasses import Location
|
||||
import typing
|
||||
|
||||
class LocData(typing.NamedTuple):
|
||||
id: int
|
||||
region: str
|
||||
score: int
|
||||
|
||||
class YachtDiceLocation(Location):
|
||||
game: str = "Yacht Dice"
|
||||
|
||||
def __init__(self, player: int, name: str, score: int, address: typing.Optional[int], parent):
|
||||
super().__init__(player, name, address, parent)
|
||||
self.yacht_dice_score = score
|
||||
self.event = not address
|
||||
|
||||
all_locations = {}
|
||||
starting_index = 16871244500 #500 more than the startin index for items
|
||||
|
||||
#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):
|
||||
location_table = {}
|
||||
for i in range(max_score+1):
|
||||
location_table[f"{i} score"] = LocData(starting_index+i, "Board", i)
|
||||
return location_table
|
||||
|
||||
#function that loads in all locations necessary for the game, so based on options.
|
||||
def ini_locations(max_score, num_locs, dif):
|
||||
location_table = {}
|
||||
|
||||
scaling = 2 #parameter that determines how many low-score location there are.
|
||||
#need more low-score locations or lower difficulties:
|
||||
if dif == 1:
|
||||
scaling = 3
|
||||
elif dif == 2:
|
||||
scaling = 2.2
|
||||
|
||||
#the scores follow the function int( 1 + (perc ** scaling) * (max_score-1) )
|
||||
#however, this will have many low values, sometimes repeating.
|
||||
#to avoid repeating scores, hiscore keeps tracks of the highest score location
|
||||
#and the next score will always be at least hiscore + 1
|
||||
#note that curscore is at most max_score-1
|
||||
hiscore = 0
|
||||
for i in range(num_locs):
|
||||
perc = (i/num_locs)
|
||||
curscore = int( 1 + (perc ** scaling) * (max_score-2) )
|
||||
if(curscore <= hiscore):
|
||||
curscore = hiscore + 1
|
||||
hiscore = curscore
|
||||
location_table[f"{curscore} score"] = LocData(starting_index + curscore, "Board", curscore)
|
||||
|
||||
#Finally, add a check for the actual max_score.
|
||||
#This is *not* counted in num_locs, since the victory items is not as well.
|
||||
location_table[f"{max_score} score"] = LocData(starting_index + max_score, "Board", max_score)
|
||||
|
||||
return location_table
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {data.id: item_name for item_name, data in all_locations.items() if data.id}
|
||||
|
||||
# we need to run this function to initialize all scores from 1 to 1000, even though not all are used
|
||||
# this in order to make sure no other worlds use any ids that are similar to Yacht Dice
|
||||
all_locations = all_locations_fun(1000)
|
||||
217
worlds/yachtdice/Options.py
Normal file
217
worlds/yachtdice/Options.py
Normal file
@@ -0,0 +1,217 @@
|
||||
from Options import Choice, Range, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
|
||||
class numberOfDiceAndRolls(Choice):
|
||||
"""
|
||||
Total number of dice and rolls in the pool.
|
||||
You start with one dice and one roll.
|
||||
This option does not change the final goal.
|
||||
"""
|
||||
display_name = "Number of dice and rolls in pool"
|
||||
option_5_dice_and_5_rolls = 5
|
||||
option_6_dice_and_4_rolls = 6
|
||||
option_7_dice_and_3_rolls = 7
|
||||
option_8_dice_and_2_rolls = 8
|
||||
default = 5
|
||||
|
||||
# class numberOfExtraRolls(Range):
|
||||
# """Total number of extra rolls you can add to your collection.
|
||||
# Wait this is always 4? Yes, I removed other options, but they might return."""
|
||||
# display_name = "Number of extra rolls"
|
||||
# range_start = 4
|
||||
# range_end = 4
|
||||
# default = 4
|
||||
|
||||
class numberDiceFragmentsPerDice(Range):
|
||||
"""
|
||||
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.
|
||||
The other dice are split into fragments, according to this setting.
|
||||
Setting this to 1 fragment per dice, just puts 'Dice' objects in the pool.
|
||||
"""
|
||||
display_name = "Number of dice fragments per dice"
|
||||
range_start = 1
|
||||
range_end = 5
|
||||
default = 4
|
||||
|
||||
|
||||
|
||||
class numberRollFragmentsPerRoll(Range):
|
||||
"""
|
||||
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.
|
||||
The other three rolls are split into fragments, according to this setting.
|
||||
Setting this to 1 fragment per roll, just puts 'Roll' objects in the pool.
|
||||
"""
|
||||
display_name = "Number of roll fragments per roll"
|
||||
range_start = 1
|
||||
range_end = 5
|
||||
default = 4
|
||||
|
||||
|
||||
class numberExtraDiceFragments(Range):
|
||||
"""
|
||||
Number of extra dice fragments in the pool.
|
||||
The number cannot exceed the number of dice fragments per dice,
|
||||
if it does, the generation will lower this setting automatically.
|
||||
The option will never give an extra full dice, but makes it easier to collect all dice.
|
||||
"""
|
||||
display_name = "Number of extra dice fragments in the pool"
|
||||
range_start = 1
|
||||
range_end = 4
|
||||
default = 3
|
||||
|
||||
|
||||
|
||||
class numberExtraRollFragments(Range):
|
||||
"""
|
||||
Number of extra roll fragments in the pool.
|
||||
The number cannot exceed the number of roll fragments per roll
|
||||
if it does, the generation will lower this setting automatically.
|
||||
The option will never give an extra full roll, but makes it easier to collect all roll.
|
||||
"""
|
||||
display_name = "Number of extra roll fragments in the pool"
|
||||
range_start = 1
|
||||
range_end = 4
|
||||
default = 3
|
||||
|
||||
|
||||
class gameDifficulty(Choice):
|
||||
"""
|
||||
Difficulty. This setting determines how difficult the scores are to achieve.
|
||||
Easy: for beginners. No luck required, just roll the dice and have fun. Lower final goal.
|
||||
Medium: intended difficulty. If you play smart, you'll finish the game without any trouble.
|
||||
Hard: you may need to play smart, be lucky and understand the score multiplier mechanic. Higher final goal.
|
||||
Extreme: more strict logic, higher final goal. NOT RECOMMENDED FOR MULTIWORLDS.
|
||||
"""
|
||||
display_name = "Game difficulty"
|
||||
option_easy = 1
|
||||
option_medium = 2
|
||||
option_hard = 3
|
||||
option_extreme = 4
|
||||
|
||||
default = 2
|
||||
|
||||
|
||||
class goalLocationPercentage(Range):
|
||||
"""
|
||||
What percentage of checks you need to get, to 'finish' the game.
|
||||
Low percentage means you can probably 'finish' the game with some of the dice/rolls/categories.
|
||||
High percentage means you need most of the useful items, and on higher difficulties you might need them all.
|
||||
"""
|
||||
display_name = "Goal percentage location"
|
||||
range_start = 70
|
||||
range_end = 100
|
||||
default = 90
|
||||
|
||||
|
||||
|
||||
class scoreMultiplierType(Choice):
|
||||
"""
|
||||
There are 10 Score Multiplier items available.
|
||||
This options decides how the Score Multipliers work.
|
||||
Both options are of similar difficulty.
|
||||
|
||||
fixed_multiplier: every multiplier item gives you +10%.
|
||||
Every score gets multiplied with the multiplier.
|
||||
So with all score multipliers, all scores get +100%.
|
||||
|
||||
step_multiplier: every multiplier item gives you +1%.
|
||||
Your multiplier increases with this percentage after every turn.
|
||||
So in the first turn you have no multiplier, but in turn 16, you can have a +150% multiplier.
|
||||
So, save your high-scoring categories for last. This option allow for more strategic games.
|
||||
"""
|
||||
display_name = "Score multiplier type"
|
||||
option_fixed_multiplier = 1
|
||||
option_step_multiplier = 2
|
||||
default = 1
|
||||
|
||||
class pointsGameMode(Choice):
|
||||
"""
|
||||
This extra game mode shuffles many points items in the pool,
|
||||
and your goal is to reach a score of 1000.
|
||||
|
||||
yes_1_per_item: hundreds of "1 Point" items are shuffled into the pool.
|
||||
NOT recommended in multiplayer, unless everyone is aware of the hundred of extra items
|
||||
|
||||
yes_10_per_item: puts tens of "10 Points" (and a few 1 Points) into the item pool.
|
||||
|
||||
yes_100_per_item: puts a few "100 Points" (and a few 1 and 10 Points) into the item pool.
|
||||
Warning: will unlock many checks if an 100 Points item is collected.
|
||||
"""
|
||||
display_name = "Extra points game mode"
|
||||
option_no_thanks = 1
|
||||
option_yes_1_per_item = 2
|
||||
option_yes_10_per_item = 3
|
||||
option_yes_100_per_item = 4
|
||||
default = 1
|
||||
|
||||
class minimizeExtraItems(Choice):
|
||||
"""
|
||||
Would you like to minimize the number of extra items in the pool?
|
||||
Note that if you put this on, categories Fives, Sixes and Pair are put early into the playthrough.
|
||||
"""
|
||||
display_name = "Minimize extra items"
|
||||
option_no_dont = 1
|
||||
option_yes_please = 2
|
||||
default = 1
|
||||
|
||||
class addExtraPoints(Choice):
|
||||
"""
|
||||
Yacht Dice typically has space for more items.
|
||||
Would you like extra points shuffled in the item pool?
|
||||
They make the game a little bit easier, as they are not considered in the logic.
|
||||
all_of_it: put as many extra points in locations as possible
|
||||
sure: put some extra points in
|
||||
never: don't but any extra points
|
||||
"""
|
||||
display_name = "Extra points in the pool"
|
||||
option_all_of_it = 1
|
||||
option_sure = 2
|
||||
option_never = 3
|
||||
default = 2
|
||||
|
||||
class addStoryChapters(Choice):
|
||||
"""
|
||||
Yacht Dice typically has space for more items.
|
||||
Would you like story chapters shuffled in the item pool?
|
||||
Note: if you have extra points on "all_of_it" there won't be story chapters.
|
||||
"""
|
||||
display_name = "Extra story chapters in the pool"
|
||||
option_all_of_it = 1
|
||||
option_sure = 2
|
||||
option_never = 3
|
||||
default = 2
|
||||
|
||||
class whichStory(Choice):
|
||||
"""
|
||||
The most important part of Yacht Dice is the narrative.
|
||||
10 story chapters are shuffled into the item pool.
|
||||
You can read them in the feed on the website.
|
||||
Which story would you like to read?
|
||||
"""
|
||||
display_name = "Story"
|
||||
option_the_quest_of_the_dice_warrior = 1
|
||||
option_the_tragedy_of_fortunas_gambit = 2
|
||||
option_the_dicey_animal_dice_game = 3
|
||||
option_whispers_of_fate = 4
|
||||
option_a_yacht_dice_odyssey = 5
|
||||
option_a_rollin_rhyme_adventure = 6
|
||||
option_random_story = -1
|
||||
default = -1
|
||||
|
||||
@dataclass
|
||||
class YachtDiceOptions(PerGameCommonOptions):
|
||||
number_of_dice_and_rolls: numberOfDiceAndRolls
|
||||
number_of_dice_fragments_per_dice: numberDiceFragmentsPerDice
|
||||
number_of_extra_dice_fragments: numberExtraDiceFragments
|
||||
number_of_roll_fragments_per_roll: numberRollFragmentsPerRoll
|
||||
number_of_extra_roll_fragments: numberExtraRollFragments
|
||||
game_difficulty: gameDifficulty
|
||||
goal_location_percentage: goalLocationPercentage
|
||||
score_multiplier_type: scoreMultiplierType
|
||||
points_game_mode: pointsGameMode
|
||||
minimize_extra_items: minimizeExtraItems
|
||||
add_extra_points: addExtraPoints
|
||||
add_story_chapters: addStoryChapters
|
||||
which_story: whichStory
|
||||
196
worlds/yachtdice/Rules.py
Normal file
196
worlds/yachtdice/Rules.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from ..generic.Rules import set_rule
|
||||
from BaseClasses import MultiWorld
|
||||
from .YachtWeights import yacht_weights
|
||||
import math
|
||||
|
||||
#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.
|
||||
#This simulation has a good strategy for locking dice.
|
||||
#This gives rise to an approximate discrete distribution per category.
|
||||
#We calculate the distribution of the total score.
|
||||
#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.
|
||||
|
||||
class Category:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
#return mean score of a category
|
||||
def meanScore(self, nbDice, nbRolls):
|
||||
if nbDice == 0 or nbRolls == 0:
|
||||
return 0
|
||||
meanScore = 0
|
||||
for key in yacht_weights[self.name, min(8,nbDice), min(8,nbRolls)]:
|
||||
meanScore += key*yacht_weights[self.name, min(8,nbDice), min(8,nbRolls)][key]/100000
|
||||
return meanScore
|
||||
|
||||
|
||||
|
||||
def extractProgression(state, player, options):
|
||||
#method to obtain a list of what items the player has.
|
||||
#this includes categories, dice, rolls and score multiplier.
|
||||
|
||||
number_of_dice = (
|
||||
state.count("Dice", player)
|
||||
+ state.count("Dice Fragment", player) // options.number_of_dice_fragments_per_dice.value
|
||||
)
|
||||
|
||||
number_of_rerolls = (
|
||||
state.count("Roll", player)
|
||||
+ state.count("Roll Fragment", player) // options.number_of_roll_fragments_per_roll.value
|
||||
)
|
||||
|
||||
number_of_mults = state.count("Score Multiplier", player)
|
||||
|
||||
|
||||
score_mult = -10000
|
||||
if options.score_multiplier_type.value == 1: #fixed
|
||||
score_mult = 0.1 * number_of_mults
|
||||
if options.score_multiplier_type.value == 2: #step
|
||||
score_mult = 0.01 * number_of_mults
|
||||
|
||||
categories = []
|
||||
|
||||
if state.has("Category Choice", player, 1):
|
||||
categories.append(Category("Choice"))
|
||||
if state.has("Category Inverse Choice", player, 1):
|
||||
categories.append(Category("Choice"))
|
||||
if state.has("Category Sixes", player, 1):
|
||||
categories.append(Category("Sixes"))
|
||||
if state.has("Category Fives", player, 1):
|
||||
categories.append(Category("Fives"))
|
||||
if state.has("Category Tiny Straight", player, 1):
|
||||
categories.append(Category("TinyStraight"))
|
||||
if state.has("Category Threes", player, 1):
|
||||
categories.append(Category("Threes"))
|
||||
if state.has("Category Fours", player, 1):
|
||||
categories.append(Category("Fours"))
|
||||
if state.has("Category Pair", player, 1):
|
||||
categories.append(Category("Pair"))
|
||||
if state.has("Category Three of a Kind", player, 1):
|
||||
categories.append(Category("ThreeOfAKind"))
|
||||
if state.has("Category Four of a Kind", player, 1):
|
||||
categories.append(Category("FourOfAKind"))
|
||||
if state.has("Category Ones", player, 1):
|
||||
categories.append(Category("Ones"))
|
||||
if state.has("Category Twos", player, 1):
|
||||
categories.append(Category("Twos"))
|
||||
if state.has("Category Small Straight", player, 1):
|
||||
categories.append(Category("SmallStraight"))
|
||||
if state.has("Category Large Straight", player, 1):
|
||||
categories.append(Category("LargeStraight"))
|
||||
if state.has("Category Full House", player, 1):
|
||||
categories.append(Category("FullHouse"))
|
||||
if state.has("Category Yacht", player, 1):
|
||||
categories.append(Category("Yacht"))
|
||||
|
||||
|
||||
extra_points_in_logic = state.count("1 Point", player)
|
||||
extra_points_in_logic += state.count("10 Points", player) * 10
|
||||
extra_points_in_logic += state.count("100 Points", player) * 100
|
||||
|
||||
|
||||
return [categories, number_of_dice, number_of_rerolls, score_mult, extra_points_in_logic]
|
||||
|
||||
#We will store the results of this function as it is called often for the same parameters.
|
||||
yachtdice_cache = {}
|
||||
|
||||
#Function that returns the feasible score in logic based on items obtained.
|
||||
def diceSimulationStrings(categories, nbDice, nbRolls, multiplier, diff, scoremulttype):
|
||||
tup = tuple([tuple(sorted([c.name for c in categories])), nbDice, nbRolls, multiplier]) #identifier
|
||||
|
||||
#if already computed, return the result
|
||||
if tup in yachtdice_cache.keys():
|
||||
return yachtdice_cache[tup]
|
||||
|
||||
#sort categories because for the step multiplier, you will want low-scorig categories first
|
||||
categories.sort(key=lambda category: category.meanScore(nbDice, nbRolls))
|
||||
|
||||
#function to add two discrete distribution.
|
||||
def add_distributions(dist1, dist2, mult):
|
||||
combined_dist = {}
|
||||
for val1, prob1 in dist1.items():
|
||||
for val2, prob2 in dist2.items():
|
||||
if int(val1 + val2 * mult) in combined_dist.keys():
|
||||
combined_dist[int(val1 + val2 * mult)] += prob1 * prob2
|
||||
else:
|
||||
combined_dist[int(val1 + val2 * mult)] = prob1 * prob2
|
||||
return combined_dist
|
||||
|
||||
#function to take the maximum of 'times' i.i.d. dist1.
|
||||
def max_dist(dist1, times):
|
||||
new_dist = {0: 1}
|
||||
for _ in range(times):
|
||||
c = new_dist.copy()
|
||||
new_dist = {}
|
||||
for val1, prob1 in c.items():
|
||||
for val2, prob2 in dist1.items():
|
||||
new_val = max(val1, val2)
|
||||
new_prob = prob1 * prob2
|
||||
|
||||
# Update the probability for the new value
|
||||
if new_val in new_dist:
|
||||
new_dist[new_val] += new_prob
|
||||
else:
|
||||
new_dist[new_val] = new_prob
|
||||
|
||||
return new_dist
|
||||
|
||||
#Returns percentile value of a distribution.
|
||||
def percentile_distribution(dist, percentile):
|
||||
sorted_values = sorted(dist.keys())
|
||||
cumulative_prob = 0
|
||||
prev_val = None
|
||||
|
||||
for val in sorted_values:
|
||||
prev_val = val
|
||||
cumulative_prob += dist[val]
|
||||
if cumulative_prob >= percentile:
|
||||
return prev_val # Return the value before reaching the desired percentile
|
||||
|
||||
# Return the first value if percentile is lower than all probabilities
|
||||
return prev_val if prev_val is not None else sorted_values[0]
|
||||
|
||||
#calculate total distribution
|
||||
total_dist = {0: 1}
|
||||
for j in range(len(categories)):
|
||||
if nbDice == 0 or nbRolls == 0:
|
||||
dist = {0: 100000}
|
||||
else:
|
||||
dist = yacht_weights[categories[j].name, min(8,nbDice), min(8,nbRolls)].copy()
|
||||
|
||||
for key in dist.keys():
|
||||
dist[key] /= 100000
|
||||
|
||||
#for higher difficulties, the simulation gets multiple tries for categories.
|
||||
dist = max_dist(dist, max(1, len(categories) // (6 - diff)))
|
||||
|
||||
cur_mult = -100
|
||||
if scoremulttype == 1: #fixed
|
||||
cur_mult = multiplier
|
||||
if scoremulttype == 2: #step
|
||||
cur_mult = j * multiplier
|
||||
total_dist = add_distributions(total_dist, dist, 1 + cur_mult )
|
||||
|
||||
#save result into the cache, then return it
|
||||
yachtdice_cache[tup] = math.floor(percentile_distribution(total_dist, .40))
|
||||
return yachtdice_cache[tup]
|
||||
|
||||
# Returns the feasible score that one can reach with the current state, options and difficulty.
|
||||
def diceSimulation(state, player, options):
|
||||
categories, nbDice, nbRolls, multiplier, expoints = extractProgression(state, player, options)
|
||||
return diceSimulationStrings(categories, nbDice, nbRolls, multiplier,
|
||||
options.game_difficulty.value, options.score_multiplier_type.value) + expoints
|
||||
|
||||
# Sets rules on entrances and advancements that are always applied
|
||||
def set_yacht_rules(world: MultiWorld, player: int, options):
|
||||
for l in world.get_locations(player):
|
||||
set_rule(l,
|
||||
lambda state,
|
||||
curscore=l.yacht_dice_score,
|
||||
player=player:
|
||||
diceSimulation(state, player, options) >= curscore)
|
||||
|
||||
# Sets rules on completion condition
|
||||
def set_yacht_completion_rules(world: MultiWorld, player: int):
|
||||
world.completion_condition[player] = lambda state: state.has("Victory", player)
|
||||
8
worlds/yachtdice/YachtWeights.py
Normal file
8
worlds/yachtdice/YachtWeights.py
Normal file
File diff suppressed because one or more lines are too long
383
worlds/yachtdice/__init__.py
Normal file
383
worlds/yachtdice/__init__.py
Normal file
@@ -0,0 +1,383 @@
|
||||
from BaseClasses import Region, Entrance, Item, Tutorial
|
||||
from .Items import YachtDiceItem, item_table
|
||||
from .Locations import YachtDiceLocation, all_locations, ini_locations
|
||||
from .Options import YachtDiceOptions
|
||||
from .Rules import set_yacht_rules, set_yacht_completion_rules
|
||||
from ..AutoWorld import World, WebWorld
|
||||
import math
|
||||
import logging
|
||||
|
||||
|
||||
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 game's depths.
|
||||
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()}
|
||||
|
||||
ap_world_version = "1.0.1"
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
def create_items(self):
|
||||
|
||||
#number of dice and rolls in the pull
|
||||
numDice = self.options.number_of_dice_and_rolls.value # either 5, 6, 7 or 8
|
||||
numRolls = 10 - numDice # either 5, 4, 3 en 2
|
||||
|
||||
#amount of dice and roll fragments needed to get a dice or roll
|
||||
amDiceF = self.options.number_of_dice_fragments_per_dice.value
|
||||
amRollsF = self.options.number_of_roll_fragments_per_roll.value
|
||||
|
||||
#number of extra dice and roll fragments in the pool,
|
||||
#so that you don't have to wait for that one last fragment
|
||||
#capped to be one less than number of fragments needed to complete a new dice/roll.
|
||||
exDiceF = max(0, min(amDiceF - 1, self.options.number_of_extra_dice_fragments.value) )
|
||||
exRollsF = max(0, min(amRollsF - 1, self.options.number_of_extra_roll_fragments.value) )
|
||||
|
||||
#Start the game with one dice, one roll, category choice and category inverse choice.
|
||||
self.multiworld.push_precollected(self.create_item("Dice"))
|
||||
self.multiworld.push_precollected(self.create_item("Roll"))
|
||||
self.multiworld.push_precollected(self.create_item("Category Choice"))
|
||||
self.multiworld.push_precollected(self.create_item("Category Inverse Choice"))
|
||||
|
||||
|
||||
|
||||
# Generate item pool. First add necessary items. Later complete the itempool to match locations.
|
||||
itempool = []
|
||||
|
||||
|
||||
#if one fragment per dice, just add "Dice" objects
|
||||
if amDiceF == 1:
|
||||
itempool += ["Dice"] * (numDice-1) #minus one because one is in start inventory
|
||||
else:
|
||||
itempool += ["Dice"] #always add a full dice to make generation easier
|
||||
#add dice fragments, note the -2 because one is added in the previous line, one is in start inventory
|
||||
itempool += ["Dice Fragment"] * (amDiceF * (numDice-2) + exDiceF)
|
||||
|
||||
#if one fragment per roll, just add "Roll" objects
|
||||
if amRollsF == 1:
|
||||
itempool += ["Roll"] * (numRolls-1) #minus one because one is in start inventory
|
||||
else:
|
||||
itempool += ["Roll"] #always add a full roll to make generation easier
|
||||
#add roll fragments, note the -2 because one is added in the previous line, one is in start inventory
|
||||
itempool += ["Roll Fragment"] * (amRollsF * (numRolls-2) + exRollsF)
|
||||
|
||||
#always add exactly 10 score multipliers
|
||||
itempool += ["Score Multiplier"] * 10
|
||||
|
||||
#add all categories. Note: not "choice" and "inverse choice", they are obtained at the start
|
||||
itempool += ["Category Ones"]
|
||||
itempool += ["Category Twos"]
|
||||
itempool += ["Category Threes"]
|
||||
itempool += ["Category Fours"]
|
||||
itempool += ["Category Fives"]
|
||||
itempool += ["Category Sixes"]
|
||||
itempool += ["Category Pair"]
|
||||
itempool += ["Category Three of a Kind"]
|
||||
itempool += ["Category Four of a Kind"]
|
||||
itempool += ["Category Tiny Straight"]
|
||||
itempool += ["Category Small Straight"]
|
||||
itempool += ["Category Large Straight"]
|
||||
itempool += ["Category Full House"]
|
||||
itempool += ["Category Yacht"]
|
||||
|
||||
if(self.options.points_game_mode.value >= 4):
|
||||
while self.extra_points_for_game_mode >= 89: #rather have 100 points than lot of smaller items
|
||||
itempool += ["100 Points"]
|
||||
self.extra_points_for_game_mode -= 100
|
||||
|
||||
if(self.options.points_game_mode.value >= 3):
|
||||
while self.extra_points_for_game_mode >= 7: #rather have 10 points that lot of 1 points.
|
||||
itempool += ["10 Points"]
|
||||
self.extra_points_for_game_mode -= 10
|
||||
|
||||
if(self.options.points_game_mode.value >= 2 and self.extra_points_for_game_mode > 0):
|
||||
itempool += ["1 Point"] * self.extra_points_for_game_mode
|
||||
|
||||
#count the number of locations in the game. extra_plando_items is set in generate_early
|
||||
#and counts number of plando items *not* from pool.
|
||||
|
||||
already_items = len(itempool) + self.extra_plando_items
|
||||
|
||||
#the number of necessary items, should never exceed the number_of_locations
|
||||
#if it does, there is some weird error, perhaps with plando. This should raise an error...
|
||||
if already_items > self.number_of_locations:
|
||||
logging.error(f"In Yacht Dice, there are more items \
|
||||
than locations ({already_items}, {self.number_of_locations})")
|
||||
|
||||
#note that self.number_of_locations is the number of locations EXCLUDING the victory location.
|
||||
#and since the victory item is added later, we should have the number of items
|
||||
#equal self.number_of_locations
|
||||
|
||||
#From here, we'll count the number of items in the itempool, and add 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_extra_points.value == 1: #all of the extra points
|
||||
already_items = len(itempool) + self.extra_plando_items
|
||||
itempool += ["Extra Point"] * min(self.number_of_locations - already_items, 100)
|
||||
|
||||
#first, 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(itempool) + self.extra_plando_items
|
||||
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
|
||||
itempool += ["Story Chapter"] * number_of_items
|
||||
|
||||
#add some extra points (useful)
|
||||
if self.options.add_extra_points.value == 2: #add extra points if wanted
|
||||
already_items = len(itempool) + self.extra_plando_items
|
||||
itempool += ["Extra 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(itempool) + self.extra_plando_items
|
||||
if(self.number_of_locations - already_items >= 10):
|
||||
itempool += ["Story Chapter"] * 10
|
||||
|
||||
#add some extra points if there is still room
|
||||
if self.options.add_extra_points.value == 2:
|
||||
already_items = len(itempool) + self.extra_plando_items
|
||||
itempool += ["Extra Point"] * min(self.number_of_locations - already_items, 10)
|
||||
|
||||
#add some encouragements filler-items if there is still room
|
||||
already_items = len(itempool) + self.extra_plando_items
|
||||
itempool += ["Encouragement"] * min(self.number_of_locations - already_items, 5)
|
||||
|
||||
#add some fun facts filler-items if there is still room
|
||||
already_items = len(itempool) + self.extra_plando_items
|
||||
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 don't do anything.
|
||||
|
||||
#probability of Good and Bad rng, based on difficulty for fun :)
|
||||
p = 0.5
|
||||
if self.options.game_difficulty.value == 1:
|
||||
p = 0.9
|
||||
elif self.options.game_difficulty.value == 2:
|
||||
p = 0.7
|
||||
elif self.options.game_difficulty.value == 3:
|
||||
p = 0.5
|
||||
elif self.options.game_difficulty.value == 4:
|
||||
p = 0.1
|
||||
|
||||
already_items = len(itempool) + self.extra_plando_items
|
||||
itempool += ["Good RNG"
|
||||
for _ in range(self.number_of_locations - already_items)]
|
||||
|
||||
#we're done adding items. Now because of the last step, number of items should be number of locations
|
||||
already_items = len(itempool) + self.extra_plando_items
|
||||
if len(itempool) != self.number_of_locations:
|
||||
logging.error(f"Number in itempool is not number of locations {len(itempool)} {self.number_of_locations}.")
|
||||
|
||||
#convert strings to actual items
|
||||
itempool = [item for item in map(lambda name: self.create_item(name), itempool)]
|
||||
|
||||
#and add them to the itempool
|
||||
for item in itempool:
|
||||
self.multiworld.itempool += [item]
|
||||
|
||||
|
||||
|
||||
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 generate_early(self):
|
||||
#calculate the maximum score goal:
|
||||
game_difficulty = self.options.game_difficulty.value
|
||||
|
||||
self.max_score = 500
|
||||
if game_difficulty == 1:
|
||||
self.max_score = 400
|
||||
elif game_difficulty == 2:
|
||||
self.max_score = 500
|
||||
elif game_difficulty == 3:
|
||||
self.max_score = 630
|
||||
elif game_difficulty == 4:
|
||||
self.max_score = 683
|
||||
|
||||
self.extra_points_for_game_mode = 0
|
||||
if(self.options.points_game_mode.value >= 2):
|
||||
self.extra_points_for_game_mode = 1000 - self.max_score
|
||||
self.max_score = 1000
|
||||
|
||||
|
||||
|
||||
#in generate early, we calculate the number of locations necessary, based on yaml options.
|
||||
|
||||
numDice = self.options.number_of_dice_and_rolls.value
|
||||
numRolls = 10 - numDice
|
||||
|
||||
amDiceF = self.options.number_of_dice_fragments_per_dice.value
|
||||
amRollsF = self.options.number_of_roll_fragments_per_roll.value
|
||||
|
||||
exDiceF = max(0, min(amDiceF - 1, self.options.number_of_extra_dice_fragments.value) )
|
||||
exRollsF = max(0, min(amRollsF - 1, self.options.number_of_extra_roll_fragments.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())
|
||||
|
||||
#number of locations should be at least number of items
|
||||
#per line, dice, rolls, score multipliers, categories, plando items, victory item, extra points
|
||||
min_number_of_locations = 1 + (numDice - 2) * amDiceF + exDiceF \
|
||||
+ 1 + (numRolls - 2) * amRollsF + exRollsF \
|
||||
+ 10 \
|
||||
+ 16 \
|
||||
+ self.extra_plando_items \
|
||||
+ 1
|
||||
|
||||
|
||||
|
||||
#We need more locations with other items to make sure generation works.
|
||||
#with single-player, we add 40%, which minimized generation fails.
|
||||
|
||||
#When there are more worlds, we can lower the extra percentage of locations.
|
||||
|
||||
#If Yacht Dice is matched with ONLY games with few locations like Clique,
|
||||
# there is a very small chance of gen failure (around 1%)
|
||||
#But otherwise it generates fine :)
|
||||
|
||||
if self.options.minimize_extra_items.value == 2:
|
||||
extraPercentage = max(1.1, 1.5 - self.multiworld.players / 10)
|
||||
else:
|
||||
extraPercentage = 1.7
|
||||
|
||||
min_number_of_locations = max(min_number_of_locations + 10,
|
||||
math.ceil(min_number_of_locations * extraPercentage))
|
||||
|
||||
if(self.options.points_game_mode == 2):
|
||||
min_number_of_locations += self.extra_points_for_game_mode
|
||||
if(self.options.points_game_mode == 3):
|
||||
min_number_of_locations += self.extra_points_for_game_mode // 10 + 10
|
||||
if(self.options.points_game_mode == 4):
|
||||
min_number_of_locations += self.extra_points_for_game_mode // 100 + 20
|
||||
|
||||
#then to make sure generation works, we need to add locations, in case important items are placed late
|
||||
#add at least 10 locations or 20%.
|
||||
self.number_of_locations = min_number_of_locations
|
||||
|
||||
def create_regions(self):
|
||||
#we have no complicated regions, just one rule per location.
|
||||
|
||||
game_difficulty = self.options.game_difficulty.value
|
||||
|
||||
#set the maximum score for which there is a check.
|
||||
|
||||
|
||||
#call the ini_locations function, that generations locations based on the inputs.
|
||||
location_table = ini_locations(self.max_score, self.number_of_locations, game_difficulty)
|
||||
|
||||
#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]
|
||||
|
||||
#parameter to see where the final check should be
|
||||
goal_percentage_location = self.options.goal_location_percentage.value
|
||||
|
||||
#which index of all locations should have the Victory item.
|
||||
victory_id = int(goal_percentage_location / 100 * len(board.locations))-1
|
||||
|
||||
# Add the victory item to the correct location.
|
||||
# The website declares that the game is complete when the victory item is obtained.
|
||||
board.locations[victory_id].place_locked_item(self.create_item("Victory"))
|
||||
|
||||
#these will be slot_data input
|
||||
self.goal_score = board.locations[victory_id].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 pre_fill(self):
|
||||
|
||||
#in the pre_fill, make sure one dice and one roll is early, so that you'll have 2 dice and 2 rolls soon
|
||||
self.multiworld.early_items[self.player]["Dice"] = 1
|
||||
self.multiworld.early_items[self.player]["Roll"] = 1
|
||||
|
||||
|
||||
#put more items early since we want less extra items.
|
||||
if self.options.minimize_extra_items.value == 2:
|
||||
self.multiworld.early_items[self.player]["Category Pair"] = 1
|
||||
self.multiworld.early_items[self.player]["Category Fives"] = 1
|
||||
self.multiworld.early_items[self.player]["Category Sixes"] = 1
|
||||
|
||||
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(
|
||||
"number_of_dice_and_rolls",
|
||||
"number_of_dice_fragments_per_dice",
|
||||
"number_of_roll_fragments_per_roll",
|
||||
"number_of_extra_roll_fragments",
|
||||
"game_difficulty",
|
||||
"goal_location_percentage",
|
||||
"score_multiplier_type",
|
||||
"add_extra_points",
|
||||
"add_story_chapters",
|
||||
"which_story"
|
||||
)
|
||||
|
||||
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
|
||||
22
worlds/yachtdice/docs/en_YachtDice.md
Normal file
22
worlds/yachtdice/docs/en_YachtDice.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Yacht Dice
|
||||
|
||||
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 game's depths. Roll your way to victory by reaching the target score!
|
||||
|
||||
## Where is the settings page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
|
||||
|
||||
## What is considered a location check in Yacht Dice?
|
||||
|
||||
Location checks are reaching certain scores for the first time. The score target for the next check is shown on the website.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
|
||||
An item can either be extra dice, extra rolls, extra score multipliers or the unlock of a scoring category.
|
||||
These items allow for sailing towards even higher scoress for even more loot.
|
||||
There are other items too, like extra points, lore, fun facts etc.
|
||||
|
||||
## What is the victory condition?
|
||||
|
||||
Reaching the target score completes the game. There are options for setting the location of the target score. The target score is displayed on the website.
|
||||
|
||||
21
worlds/yachtdice/docs/setup_en.md
Normal file
21
worlds/yachtdice/docs/setup_en.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Yacht Dice Randomizer Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- A browser (you are probably using one right now!).
|
||||
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
|
||||
## Playing the game
|
||||
Open the Yacht Dice website. There are two options:
|
||||
- Download the latest release from [Yacht Dice Releases](https://github.com/spinerak/YachtDiceAP/releases) and unzip the Website.zip. Then open player.html in your browser.
|
||||
- Cruise over to the [Yacht Dice website](https://yacht-dice-ap.netlify.app/). This also works on mobile. If the website is not available, use the first option.
|
||||
|
||||
The website has an offline play option to try out the game without having to generate a game first.
|
||||
|
||||
## Play with Archipelago
|
||||
|
||||
- Create your yaml file via the [Yacht Dice Player Settings Page](/games/YachtDice/player-settings).
|
||||
- After generating, open the Yacht Dice website. After the tutoroll, fill in the room-information.
|
||||
- After logging in, you are good to go. The website has a built-in client, where you can chat and send commands.
|
||||
|
||||
For more information on yaml files, generating Archipelago games and connecting to servers, please see the [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en).
|
||||
Reference in New Issue
Block a user