Add the yacht dice (from other git) world to the yacht dice fork

This commit is contained in:
spinerak
2024-05-24 20:02:25 +02:00
parent 18390ecc09
commit 89b6d2e40b
8 changed files with 977 additions and 0 deletions

68
worlds/yachtdice/Items.py Normal file
View 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)
}

View 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
View 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
View 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)

File diff suppressed because one or more lines are too long

View 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

View 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.

View 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).