YachtDice: implement new game (#3482)

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

* Update .gitignore

* Removed zillion because it doesn't work

* Update .gitignore

* added zillion again...

* Now you can have 0 extra fragments

* Added alt categories, also options

* Added item categories

* Extra categories are now working! 🐶

* changed options and added exceptions

* Testing if I change the generate.py

* Revert "Testing if I change the generate.py"

This reverts commit 7c2b3df6170dcf8d8f36a1de9fcbc9dccdec81f8.

* ignore gitignore

* Delete .gitignore

* Update .gitignore

* Update .gitignore

* Update logic, added multiplicative categories

* Changed difficulties

* Update offline mode so that it works again

* Adjusted difficulty

* New version of the apworld, with 1000 as final score, always

Will still need to check difficulty and weights of adding items.
Website is not ready yet, so this version is not usable yet :)

* Changed yaml and small bug fixes

Fix when goal and max are same
Options: changed chance to weight

* no changes, just whitespaces

* changed how logic works

Now you put an array of mults and the cpu gets a couple of tries

* Changed logic, tweaked a bit too

* Preparation for 2.0

* logic tweak

* Logic for alt categories properly now

* Update setup_en.md

* Update en_YachtDice.md

* Improve performance of add_distributions

* Formatting style

* restore gitignore to APMW

* Tweaked generation parameters and methods

* Version 2.0.3

manual input option
max score in logic always 2.0.3
faster gen

* Comments and editing

* Renamed setup guide

* Improved create_items code

* init of locations: remove self.event line

* Moved setting early items to generate_early

* Add my name to CODEOWNERS

* Added Yacht Dice to the readme in list of games

* Improve performance of Yacht Dice

* newline

* Improve typing

* This is actually just slower lol

* Update worlds/yachtdice/Items.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update Options.py

* Styling

* finished text whichstory option

* removed roll and rollfragments; not used

* import; worlds not world :)

* Option groups!

* ruff styling, fix

* ruff format styling!

* styling and capitalization of options

* small comment

* Cleaned up the "state_is_a_list" a little bit

* RUFF 🐶

* Changed filling the itempool for efficiency

Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?).
And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points.

* 🐶

* Removed plando "fix"

* Changed indent of score multiplier

* faster location function

* Comments to docstrings

* fixed making location closest to goal_score be goal_score

* options format

* iterate keys and values of a dict together

* small optimization ListState

* faster collection of categories

* return arguments instead of making a list (will 🐶 later)

* Instead of turning it into a tuple, you can just make a tuple literal

* remove .keys()

* change .random and used enumerate

* some readability improvements

* Remove location "0", we don't use that one

* Remove lookup_id_to_name entirely

I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id.

* .append instead of += for single items, percentile function changed

Also an extra comment for location ids.

* remove ) too many

* Removed sorted from category list

* Hash categories (which makes it slower :( )

Maybe I messed up or misunderstood...
I'll revert this right away since it is 2x slower, probably because of sorted instead of sort?

* Revert "Hash categories (which makes it slower :( )"

This reverts commit 34f2c1aed8c8813b2d9c58896650b82a810d3578.

* temporary push: 40% faster generation test

Small changes in logic make the generation 40% faster.
I'll have to think about how big the changes are. I suspect they are rather limited.
If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here.

* Add Points item category

* Reverse changes of bad idea :)

* ruff 🐶

* Use numpy and pmf function to speed up gen

Numpy has a built-in way to sum probability mass functions (pmf).
This shaves of 60% of the generation time :D

* Revert "Use numpy and pmf function to speed up gen"

This reverts commit 9290191cb323ae92321d6c2cfcfe8c27370f439b.

* Step inbetween to change the weights

* Changed the weights to make it faster

135 -> 81 seconds on 100 random yamls

* Adjusted max_dist, split dice_simulation function

* Removed nonlocal and pass arguments instead

* Change "weight-lists" to Dict[str, float]

* Removed the return from ini_locations.

Also added explanations to cat_weights

* Choice options; dont'use .value (will ruff later)

* Only put important options in slotdata

* 🐶

* Add Dict import

* Split the cache per player, limit size to 400.

* 🐶

* added , because of style

* Update apworld version to 2.0.6

2.0.5 is the apworld I released on github to be tested
I never separately released 2.0.4.

* Multiple smaller code improvements

- changed names in YachtWeights so we don't need to translate them in Rules anymore
- we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore
-

* 🐶 ruff

* Mostly minimize_extra_items improvements

- Change logic, generation is now even faster (0.6s per default yaml).
- Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now:
 - you start with 2 dice and 2 rolls
 - there will be less locations/items at the start of you game

* ruff 🐶

* Removed printing options

* Reworded some option descriptions

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
Spineraks 2024-08-21 19:59:21 +02:00 committed by GitHub
parent eaa8156061
commit 48c6a6fb4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 4903 additions and 0 deletions

View File

@ -75,6 +75,7 @@ Currently, the following games are supported:
* Old School Runescape
* Kingdom Hearts 1
* Mega Man 2
* Yacht Dice
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@ -202,6 +202,9 @@
# The Witness
/worlds/witness/ @NewSoupVi @blastron
# Yacht Dice
/worlds/yachtdice/ @spinerak
# Yoshi's Island
/worlds/yoshisisland/ @PinkSwitch

118
worlds/yachtdice/Items.py Normal file
View File

@ -0,0 +1,118 @@
import typing
from BaseClasses import Item, ItemClassification
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 item, always placed manually at goal location
"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),
"Fixed Score Multiplier": ItemData(16871244005, ItemClassification.progression),
"Step Score Multiplier": ItemData(16871244006, 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),
# filler items
"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),
"Bonus Point": ItemData(16871244205, ItemClassification.useful), # not included in logic
# These points are included in the logic and might be necessary to progress.
"1 Point": ItemData(16871244301, ItemClassification.progression_skip_balancing),
"10 Points": ItemData(16871244302, ItemClassification.progression),
"100 Points": ItemData(16871244303, ItemClassification.progression),
}
# item groups for better hinting
item_groups = {
"Score Multiplier": {
"Step Score Multiplier",
"Fixed Score Multiplier"
},
"Categories": {
"Category Ones",
"Category Twos",
"Category Threes",
"Category Fours",
"Category Fives",
"Category Sixes",
"Category Choice",
"Category Inverse Choice",
"Category Pair",
"Category Three of a Kind",
"Category Four of a Kind",
"Category Tiny Straight",
"Category Small Straight",
"Category Large Straight",
"Category Full House",
"Category Yacht",
"Category Distincts",
"Category Two times Ones",
"Category Half of Sixes",
"Category Twos and Threes",
"Category Sum of Odds",
"Category Sum of Evens",
"Category Double Threes and Fours",
"Category Quadruple Ones and Twos",
"Category Micro Straight",
"Category Three Odds",
"Category 1-2-1 Consecutive",
"Category Three Distinct Dice",
"Category Two Pair",
"Category 2-1-2 Consecutive",
"Category Five Distinct Dice",
"Category 4&5 Full House",
},
"Points": {
"100 Points",
"10 Points",
"1 Point",
"Bonus Point"
},
}

View File

@ -0,0 +1,79 @@
import typing
from BaseClasses import Location
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
all_locations = {}
starting_index = 16871244500 # 500 more than the starting index for items (not necessary, but this is what it is now)
def all_locations_fun(max_score):
"""
Function that is called when this file is loaded, which loads in ALL possible locations, score 1 to 1000
"""
return {f"{i} score": LocData(starting_index + i, "Board", i) for i in range(1, max_score + 1)}
def ini_locations(goal_score, max_score, number_of_locations, dif, skip_early_locations, number_of_players):
"""
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
"""
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.3
scores = []
# the scores follow the function int( 1 + (percentage ** scaling) * (max_score-1) )
# however, this will have many low values, sometimes repeating.
# to avoid repeating scores, highest_score keeps tracks of the highest score location
# and the next score will always be at least highest_score + 1
# note that current_score is at most max_score-1
highest_score = 0
start_score = 0
if skip_early_locations:
scaling = 1.95
if number_of_players > 2:
scaling = max(1.2, 2.2 - number_of_players * 0.1)
for i in range(number_of_locations - 1):
percentage = i / number_of_locations
current_score = int(start_score + 1 + (percentage**scaling) * (max_score - start_score - 2))
if current_score <= highest_score:
current_score = highest_score + 1
highest_score = current_score
scores += [current_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 goal_score not in scores:
closest_num = min(scores, key=lambda x: abs(x - goal_score))
scores[scores.index(closest_num)] = goal_score
scores += [max_score]
location_table = {f"{score} score": LocData(starting_index + score, "Board", score) for score in scores}
return location_table
# we need to run this function to initialize all scores from 1 to 1000, even though not all are used
all_locations = all_locations_fun(1000)

332
worlds/yachtdice/Options.py Normal file
View File

@ -0,0 +1,332 @@
from dataclasses import dataclass
from Options import Choice, OptionGroup, PerGameCommonOptions, Range
class GameDifficulty(Choice):
"""
Difficulty. This option 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 will finish the game without any trouble.
Hard: you will need to play smart and be lucky.
Extreme: really hard mode, which requires many brain wrinkles and insane luck. NOT RECOMMENDED FOR MULTIWORLDS.
"""
display_name = "Game difficulty"
option_easy = 1
option_medium = 2
option_hard = 3
option_extreme = 4
default = 2
class ScoreForLastCheck(Range):
"""
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.
However, you can set the score for the last check to be lower. This will make the game shorter and easier.
"""
display_name = "Score for last check"
range_start = 500
range_end = 1000
default = 1000
class ScoreForGoal(Range):
"""
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, this option is changed automatically).
"""
display_name = "Score for goal"
range_start = 500
range_end = 1000
default = 777
class MinimalNumberOfDiceAndRolls(Choice):
"""
The minimal number of dice and rolls in the pool.
These are guaranteed, unlike the later items.
You can never get more than 8 dice and 5 rolls.
You start with one dice and one roll.
"""
display_name = "Minimal number of dice and rolls in pool"
option_5_dice_and_3_rolls = 2
option_5_dice_and_5_rolls = 3
option_6_dice_and_4_rolls = 4
option_7_dice_and_3_rolls = 5
option_8_dice_and_2_rolls = 6
default = 2
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 option.
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 option.
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 AlternativeCategories(Range):
"""
There are 16 default categories, but there are also 16 alternative categories.
These alternative categories can be randomly selected to replace the default categories.
They are a little strange, but can give a fun new experience.
In the game, you can hover over categories to check what they do.
This option determines the number of alternative categories in your game.
"""
display_name = "Number of alternative categories"
range_start = 0
range_end = 16
default = 0
class ChanceOfDice(Range):
"""
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.
You can set the weight for each extra progressive item in the following options.
Of course, more dice = more points!
"""
display_name = "Weight of adding Dice"
range_start = 0
range_end = 100
default = 5
class ChanceOfRoll(Range):
"""
With more rolls, you will be able to reach higher scores.
"""
display_name = "Weight of adding Roll"
range_start = 0
range_end = 100
default = 20
class ChanceOfFixedScoreMultiplier(Range):
"""
Getting a Fixed Score Multiplier will boost all future scores by 10%.
"""
display_name = "Weight of adding Fixed Score Multiplier"
range_start = 0
range_end = 100
default = 30
class ChanceOfStepScoreMultiplier(Range):
"""
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.
By default, this item is not included. It is fun however, you just need to know the above strategy.
"""
display_name = "Weight of adding Step Score Multiplier"
range_start = 0
range_end = 100
default = 0
class ChanceOfDoubleCategory(Range):
"""
This option allows categories to appear multiple times.
Each time you get a category after the first, its score value gets doubled.
"""
display_name = "Weight of adding Category copy"
range_start = 0
range_end = 100
default = 50
class ChanceOfPoints(Range):
"""
Are you tired of rolling dice countless times and tallying up points one by one, all by yourself?
Worry not, as this option will simply add some points items to the item pool!
And getting one of these points items gives you... points!
Imagine how nice it would be to find tons of them. Or even better, having others find them FOR you!
"""
display_name = "Weight of adding Points"
range_start = 0
range_end = 100
default = 20
class PointsSize(Choice):
"""
If you choose to add points to the item pool, you can choose to have many small points,
medium-size points, a few larger points, or a mix of them.
"""
display_name = "Size of points"
option_small = 1
option_medium = 2
option_large = 3
option_mix = 4
default = 2
class MinimizeExtraItems(Choice):
"""
Besides necessary items, Yacht Dice has extra useful/filler items in the item pool.
It is possible however to decrease the number of locations and extra items.
This option will:
- decrease the number of locations at the start (you'll start with 2 dice and 2 rolls).
- will limit the number of dice/roll fragments per dice/roll to 2.
- in multiplayer games, it will reduce the number of filler items.
"""
display_name = "Minimize extra items"
option_no_dont = 1
option_yes_please = 2
default = 1
class AddExtraPoints(Choice):
"""
Yacht Dice typically has space for extra items.
This option determines if bonus points are put into the item pool.
They make the game a little bit easier, as they are not considered in the logic.
All Of It: fill all locations with extra points
Sure: put some bonus points in
Never: do not put any bonus points
"""
display_name = "Extra bonus 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.
This option determines if extra story chapters are put into the item pool.
Note: if you have extra points on "all_of_it", there will not be story chapters.
All Of It: fill all locations with story chapters
Sure: if there is space left, put in 10 story chapters.
Never: do not put any story chapters in, I do not like reading (but I am glad you are reading THIS!)
"""
display_name = "Extra story chapters in the pool"
option_all_of_it = 1
option_sure = 2
option_never = 3
default = 3
class WhichStory(Choice):
"""
The most important part of Yacht Dice is the narrative.
Of course you will need to add story chapters to the item pool.
You can read story chapters in the feed on the website and there are several stories to choose from.
"""
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
class AllowManual(Choice):
"""
If allowed, players can roll IRL dice and input them manually into the game.
By sending "manual" in the chat, an input field appears where you can type your dice rolls.
Of course, we cannot check anymore if the player is playing fair.
"""
display_name = "Allow manual inputs"
option_yes_allow = 1
option_no_dont_allow = 2
default = 1
@dataclass
class YachtDiceOptions(PerGameCommonOptions):
game_difficulty: GameDifficulty
score_for_last_check: ScoreForLastCheck
score_for_goal: ScoreForGoal
minimal_number_of_dice_and_rolls: MinimalNumberOfDiceAndRolls
number_of_dice_fragments_per_dice: NumberDiceFragmentsPerDice
number_of_roll_fragments_per_roll: NumberRollFragmentsPerRoll
alternative_categories: AlternativeCategories
allow_manual_input: AllowManual
# the following options determine what extra items are shuffled into the pool:
weight_of_dice: ChanceOfDice
weight_of_roll: ChanceOfRoll
weight_of_fixed_score_multiplier: ChanceOfFixedScoreMultiplier
weight_of_step_score_multiplier: ChanceOfStepScoreMultiplier
weight_of_double_category: ChanceOfDoubleCategory
weight_of_points: ChanceOfPoints
points_size: PointsSize
minimize_extra_items: MinimizeExtraItems
add_bonus_points: AddExtraPoints
add_story_chapters: AddStoryChapters
which_story: WhichStory
yd_option_groups = [
OptionGroup(
"Extra progression items",
[
ChanceOfDice,
ChanceOfRoll,
ChanceOfFixedScoreMultiplier,
ChanceOfStepScoreMultiplier,
ChanceOfDoubleCategory,
ChanceOfPoints,
PointsSize,
],
),
OptionGroup(
"Other items",
[
MinimizeExtraItems,
AddExtraPoints,
AddStoryChapters,
WhichStory
],
),
]

239
worlds/yachtdice/Rules.py Normal file
View File

@ -0,0 +1,239 @@
import math
from collections import Counter, defaultdict
from typing import List, Optional
from BaseClasses import MultiWorld
from worlds.generic.Rules import set_rule
from .YachtWeights import yacht_weights
# This module 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, quantity=1):
self.name = name
self.quantity = quantity # how many times you have the category
# return mean score of a category
def mean_score(self, num_dice, num_rolls):
if num_dice <= 0 or num_rolls <= 0:
return 0
mean_score = 0
for key, value in yacht_weights[self.name, min(8, num_dice), min(8, num_rolls)].items():
mean_score += key * value / 100000
return mean_score * self.quantity
class ListState:
def __init__(self, state: List[str]):
self.state = state
self.item_counts = Counter(state)
def count(self, item: str, player: Optional[str] = None) -> int:
return self.item_counts[item]
def extract_progression(state, player, frags_per_dice, frags_per_roll, allowed_categories):
"""
method to obtain a list of what items the player has.
this includes categories, dice, rolls and score multiplier etc.
First, we convert the state if it's a list, so we can use state.count(item, player)
"""
if isinstance(state, list):
state = ListState(state=state)
number_of_dice = state.count("Dice", player) + state.count("Dice Fragment", player) // frags_per_dice
number_of_rerolls = state.count("Roll", player) + state.count("Roll Fragment", player) // frags_per_roll
number_of_fixed_mults = state.count("Fixed Score Multiplier", player)
number_of_step_mults = state.count("Step Score Multiplier", player)
categories = [
Category(category_name, state.count(category_name, player))
for category_name in allowed_categories
if state.count(category_name, player) # want all categories that have count >= 1
]
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,
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.
yachtdice_cache = {}
def dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mult, diff, player):
"""
Function that returns the feasible score in logic based on items obtained.
"""
tup = (
tuple([c.name + str(c.quantity) for c in categories]),
num_dice,
num_rolls,
fixed_mult,
step_mult,
diff,
) # identifier
if player not in yachtdice_cache:
yachtdice_cache[player] = {}
if tup in yachtdice_cache[player]:
return yachtdice_cache[player][tup]
# 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))
# 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)
def add_distributions(dist1, dist2):
combined_dist = defaultdict(float)
for val1, prob1 in dist1.items():
for val2, prob2 in dist2.items():
combined_dist[val1 + val2] += prob1 * prob2
return dict(combined_dist)
# function to take the maximum of "times" i.i.d. dist1.
# (I have tried using defaultdict here too but this made it slower.)
def max_dist(dist1, mults):
new_dist = {0: 1}
for mult in mults:
temp_dist = {}
for val1, prob1 in new_dist.items():
for val2, prob2 in dist1.items():
new_val = int(max(val1, val2 * mult))
new_prob = prob1 * prob2
# Update the probability for the new value
if new_val in temp_dist:
temp_dist[new_val] += new_prob
else:
temp_dist[new_val] = new_prob
new_dist = temp_dist
return new_dist
# Returns percentile value of a distribution.
def percentile_distribution(dist, percentile):
sorted_values = sorted(dist.keys())
cumulative_prob = 0
for val in sorted_values:
cumulative_prob += dist[val]
if cumulative_prob >= percentile:
return val
# Return the last value if percentile is higher than all probabilities
return sorted_values[-1]
# parameters for logic.
# 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.
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]
# calculate total distribution
total_dist = {0: 1}
for j, category in enumerate(categories):
if num_dice <= 0 or num_rolls <= 0:
dist = {0: 100000}
else:
dist = yacht_weights[category.name, min(8, num_dice), min(8, num_rolls)].copy()
for key in dist.keys():
dist[key] /= 100000
cat_mult = 2 ** (category.quantity - 1)
# for higher difficulties, the simulation gets multiple tries for categories.
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)]
dist = max_dist(dist, mults)
total_dist = add_distributions(total_dist, dist)
# save result into the cache, then return it
outcome = sum([percentile_distribution(total_dist, perc) for perc in perc_return]) / len(perc_return)
yachtdice_cache[player][tup] = max(5, math.floor(outcome)) # at least 5.
# cache management; we rarely/never need more than 400 entries. But if for some reason it became large,
# delete the first entry of the player cache.
if len(yachtdice_cache[player]) > 400:
# Remove the oldest item
oldest_tup = next(iter(yachtdice_cache[player]))
del yachtdice_cache[player][oldest_tup]
return yachtdice_cache[player][tup]
def dice_simulation_fill_pool(state, frags_per_dice, frags_per_roll, allowed_categories, difficulty, player):
"""
Returns the feasible score that one can reach with the current state, options and difficulty.
This function is called with state being a list, during filling of item pool.
"""
categories, num_dice, num_rolls, fixed_mult, step_mult, expoints = extract_progression(
state, "state_is_a_list", frags_per_dice, frags_per_roll, allowed_categories
)
return (
dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mult, difficulty, player) + expoints
)
def dice_simulation_state_change(state, player, frags_per_dice, frags_per_roll, allowed_categories, difficulty):
"""
Returns the feasible score that one can reach with the current state, options and difficulty.
This function is called with state being a AP state object, while doing access rules.
"""
if state.prog_items[player]["state_is_fresh"] == 0:
state.prog_items[player]["state_is_fresh"] = 1
categories, num_dice, num_rolls, fixed_mult, step_mult, expoints = extract_progression(
state, player, frags_per_dice, frags_per_roll, allowed_categories
)
state.prog_items[player]["maximum_achievable_score"] = (
dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mult, difficulty, player)
+ expoints
)
return state.prog_items[player]["maximum_achievable_score"]
def set_yacht_rules(world: MultiWorld, player: int, frags_per_dice, frags_per_roll, allowed_categories, difficulty):
"""
Sets rules on reaching scores
"""
for location in world.get_locations(player):
set_rule(
location,
lambda state, curscore=location.yacht_dice_score, player=player: dice_simulation_state_change(
state, player, frags_per_dice, frags_per_roll, allowed_categories, difficulty
)
>= curscore,
)
def set_yacht_completion_rules(world: MultiWorld, player: int):
"""
Sets rules on completion condition
"""
world.completion_condition[player] = lambda state: state.has("Victory", player)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,533 @@
import math
from typing import Dict
from BaseClasses import CollectionState, Entrance, Item, Region, Tutorial
from worlds.AutoWorld import WebWorld, World
from .Items import YachtDiceItem, item_groups, item_table
from .Locations import YachtDiceLocation, all_locations, ini_locations
from .Options import (
AddExtraPoints,
AddStoryChapters,
GameDifficulty,
MinimalNumberOfDiceAndRolls,
MinimizeExtraItems,
PointsSize,
YachtDiceOptions,
yd_option_groups,
)
from .Rules import dice_simulation_fill_pool, set_yacht_completion_rules, set_yacht_rules
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"],
)
]
option_groups = yd_option_groups
class YachtDiceWorld(World):
"""
Yacht Dice is a straightforward game, custom-made for Archipelago,
where you cast your dice to chart a course for high scores,
unlocking valuable treasures along the way.
Discover more dice, extra rolls, multipliers,
and unlockable categories to navigate the depths of the game.
Roll your way to victory by reaching the target score!
"""
game: str = "Yacht Dice"
options_dataclass = YachtDiceOptions
web = YachtDiceWeb()
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {name: data.id for name, data in all_locations.items()}
item_name_groups = item_groups
ap_world_version = "2.1.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 generate_early(self):
"""
In generate early, we fill the item-pool, then determine the number of locations, and add filler items.
"""
self.itempool = []
self.precollected = []
# number of dice and rolls in the pull
opt_dice_and_rolls = self.options.minimal_number_of_dice_and_rolls
if opt_dice_and_rolls == MinimalNumberOfDiceAndRolls.option_5_dice_and_3_rolls:
num_of_dice = 5
num_of_rolls = 3
elif opt_dice_and_rolls == MinimalNumberOfDiceAndRolls.option_5_dice_and_5_rolls:
num_of_dice = 5
num_of_rolls = 5
elif opt_dice_and_rolls == MinimalNumberOfDiceAndRolls.option_6_dice_and_4_rolls:
num_of_dice = 6
num_of_rolls = 4
elif opt_dice_and_rolls == MinimalNumberOfDiceAndRolls.option_7_dice_and_3_rolls:
num_of_dice = 7
num_of_rolls = 3
elif opt_dice_and_rolls == MinimalNumberOfDiceAndRolls.option_8_dice_and_2_rolls:
num_of_dice = 8
num_of_rolls = 2
else:
raise Exception(f"[Yacht Dice] Unknown MinimalNumberOfDiceAndRolls options {opt_dice_and_rolls}")
# amount of dice and roll fragments needed to get a dice or roll
self.frags_per_dice = self.options.number_of_dice_fragments_per_dice.value
self.frags_per_roll = self.options.number_of_roll_fragments_per_roll.value
if self.options.minimize_extra_items == MinimizeExtraItems.option_yes_please:
self.frags_per_dice = min(self.frags_per_dice, 2)
self.frags_per_roll = min(self.frags_per_roll, 2)
# set difficulty
diff_value = self.options.game_difficulty
if diff_value == GameDifficulty.option_easy:
self.difficulty = 1
elif diff_value == GameDifficulty.option_medium:
self.difficulty = 2
elif diff_value == GameDifficulty.option_hard:
self.difficulty = 3
elif diff_value == GameDifficulty.option_extreme:
self.difficulty = 4
else:
raise Exception(f"[Yacht Dice] Unknown GameDifficulty options {diff_value}")
# Create a list with the specified number of 1s
num_ones = self.options.alternative_categories.value
categorylist = [1] * num_ones + [0] * (16 - num_ones)
# Shuffle the list to randomize the order
self.random.shuffle(categorylist)
# A list of all possible categories.
# Every entry in the list has two categories, one 'default' category and one 'alt'.
# You get either of the two for every entry, so a total of 16 unique categories.
all_categories = [
["Category Choice", "Category Double Threes and Fours"],
["Category Inverse Choice", "Category Quadruple Ones and Twos"],
["Category Ones", "Category Distincts"],
["Category Twos", "Category Two times Ones"],
["Category Threes", "Category Half of Sixes"],
["Category Fours", "Category Twos and Threes"],
["Category Fives", "Category Sum of Odds"],
["Category Sixes", "Category Sum of Evens"],
["Category Pair", "Category Micro Straight"],
["Category Three of a Kind", "Category Three Odds"],
["Category Four of a Kind", "Category 1-2-1 Consecutive"],
["Category Tiny Straight", "Category Three Distinct Dice"],
["Category Small Straight", "Category Two Pair"],
["Category Large Straight", "Category 2-1-2 Consecutive"],
["Category Full House", "Category Five Distinct Dice"],
["Category Yacht", "Category 4&5 Full House"],
]
# categories used in this game.
self.possible_categories = []
for index, cats in enumerate(all_categories):
self.possible_categories.append(cats[categorylist[index]])
# Add Choice and Inverse choice (or their alts) to the precollected list.
if index == 0 or index == 1:
self.precollected.append(cats[categorylist[index]])
else:
self.itempool.append(cats[categorylist[index]])
# Also start with one Roll and one Dice
self.precollected.append("Dice")
num_of_dice_to_add = num_of_dice - 1
self.precollected.append("Roll")
num_of_rolls_to_add = num_of_rolls - 1
self.skip_early_locations = False
if self.options.minimize_extra_items == MinimizeExtraItems.option_yes_please:
self.precollected.append("Dice")
num_of_dice_to_add -= 1
self.precollected.append("Roll")
num_of_rolls_to_add -= 1
self.skip_early_locations = True
if num_of_dice_to_add > 0:
self.itempool.append("Dice")
num_of_dice_to_add -= 1
if num_of_rolls_to_add > 0:
self.itempool.append("Roll")
num_of_rolls_to_add -= 1
# if one fragment per dice, just add "Dice" objects
if num_of_dice_to_add > 0:
if self.frags_per_dice == 1:
self.itempool += ["Dice"] * num_of_dice_to_add # minus one because one is in start inventory
else:
self.itempool += ["Dice Fragment"] * (self.frags_per_dice * num_of_dice_to_add)
# if one fragment per roll, just add "Roll" objects
if num_of_rolls_to_add > 0:
if self.frags_per_roll == 1:
self.itempool += ["Roll"] * num_of_rolls_to_add # minus one because one is in start inventory
else:
self.itempool.append("Roll") # always add a full roll to make generation easier (will be early)
self.itempool += ["Roll Fragment"] * (self.frags_per_roll * num_of_rolls_to_add)
already_items = len(self.itempool)
# Yacht Dice needs extra filler items so it doesn't get stuck in generation.
# For now, we calculate the number of extra items we'll need later.
if self.options.minimize_extra_items == MinimizeExtraItems.option_yes_please:
extra_percentage = max(0.1, 0.8 - self.multiworld.players / 10)
elif self.options.minimize_extra_items == MinimizeExtraItems.option_no_dont:
extra_percentage = 0.72
else:
raise Exception(f"[Yacht Dice] Unknown MinimizeExtraItems options {self.options.minimize_extra_items}")
extra_locations_needed = max(10, math.ceil(already_items * extra_percentage))
# max score is the value of the last check. Goal score is the score needed to 'finish' the game
self.max_score = self.options.score_for_last_check.value
self.goal_score = min(self.max_score, self.options.score_for_goal.value)
# Yacht Dice adds items into the pool until a score of at least 1000 is reached.
# the yaml contains weights, which determine how likely it is that specific items get added.
# If all weights are 0, some of them will be made to be non-zero later.
weights: Dict[str, float] = {
"Dice": self.options.weight_of_dice.value,
"Roll": self.options.weight_of_roll.value,
"Fixed Score Multiplier": self.options.weight_of_fixed_score_multiplier.value,
"Step Score Multiplier": self.options.weight_of_step_score_multiplier.value,
"Double category": self.options.weight_of_double_category.value,
"Points": self.options.weight_of_points.value,
}
# if the player wants extra rolls or dice, fill the pool with fragments until close to an extra roll/dice
if weights["Dice"] > 0 and self.frags_per_dice > 1:
self.itempool += ["Dice Fragment"] * (self.frags_per_dice - 1)
if weights["Roll"] > 0 and self.frags_per_roll > 1:
self.itempool += ["Roll Fragment"] * (self.frags_per_roll - 1)
# calibrate the weights, since the impact of each of the items is different
weights["Dice"] = weights["Dice"] / 5 * self.frags_per_dice
weights["Roll"] = weights["Roll"] / 5 * self.frags_per_roll
extra_points_added = 0
multipliers_added = 0
items_added = 0
def get_item_to_add(weights, extra_points_added, multipliers_added, items_added):
items_added += 1
all_items = self.itempool + self.precollected
dice_fragments_in_pool = all_items.count("Dice") * self.frags_per_dice + all_items.count("Dice Fragment")
if dice_fragments_in_pool + 1 >= 9 * self.frags_per_dice:
weights["Dice"] = 0 # don't allow >=9 dice
roll_fragments_in_pool = all_items.count("Roll") * self.frags_per_roll + all_items.count("Roll Fragment")
if roll_fragments_in_pool + 1 >= 6 * self.frags_per_roll:
weights["Roll"] = 0 # don't allow >= 6 rolls
# Don't allow too many multipliers
if multipliers_added > 50:
weights["Fixed Score Multiplier"] = 0
weights["Step Score Multiplier"] = 0
# Don't allow too many extra points
if extra_points_added > 300:
weights["Points"] = 0
# if all weights are zero, allow to add fixed score multiplier, double category, points.
if sum(weights.values()) == 0:
if multipliers_added <= 50:
weights["Fixed Score Multiplier"] = 1
weights["Double category"] = 1
if extra_points_added <= 300:
weights["Points"] = 1
# Next, add the appropriate item. We'll slightly alter weights to avoid too many of the same item
which_item_to_add = self.random.choices(list(weights.keys()), weights=list(weights.values()))[0]
if which_item_to_add == "Dice":
weights["Dice"] /= 1 + self.frags_per_dice
return "Dice" if self.frags_per_dice == 1 else "Dice Fragment"
elif which_item_to_add == "Roll":
weights["Roll"] /= 1 + self.frags_per_roll
return "Roll" if self.frags_per_roll == 1 else "Roll Fragment"
elif which_item_to_add == "Fixed Score Multiplier":
weights["Fixed Score Multiplier"] /= 1.05
multipliers_added += 1
return "Fixed Score Multiplier"
elif which_item_to_add == "Step Score Multiplier":
weights["Step Score Multiplier"] /= 1.1
multipliers_added += 1
return "Step Score Multiplier"
elif which_item_to_add == "Double category":
# Below entries are the weights to add each category.
# Prefer to add choice or number categories, because the other categories are too "all or nothing",
# which often don't give any points, until you get overpowered, and then they give all points.
cat_weights = [2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1]
weights["Double category"] /= 1.1
return self.random.choices(self.possible_categories, weights=cat_weights)[0]
elif which_item_to_add == "Points":
score_dist = self.options.points_size
probs = {"1 Point": 1, "10 Points": 0, "100 Points": 0}
if score_dist == PointsSize.option_small:
probs = {"1 Point": 0.9, "10 Points": 0.1, "100 Points": 0}
elif score_dist == PointsSize.option_medium:
probs = {"1 Point": 0, "10 Points": 1, "100 Points": 0}
elif score_dist == PointsSize.option_large:
probs = {"1 Point": 0, "10 Points": 0.3, "100 Points": 0.7}
elif score_dist == PointsSize.option_mix:
probs = {"1 Point": 0.3, "10 Points": 0.4, "100 Points": 0.3}
else:
raise Exception(f"[Yacht Dice] Unknown PointsSize options {score_dist}")
choice = self.random.choices(list(probs.keys()), weights=list(probs.values()))[0]
if choice == "1 Point":
weights["Points"] /= 1.01
extra_points_added += 1
return "1 Point"
elif choice == "10 Points":
weights["Points"] /= 1.1
extra_points_added += 10
return "10 Points"
elif choice == "100 Points":
weights["Points"] /= 2
extra_points_added += 100
return "100 Points"
else:
raise Exception("Unknown point value (Yacht Dice)")
else:
raise Exception(f"Invalid index when adding new items in Yacht Dice: {which_item_to_add}")
# adding 17 items as a start seems like the smartest way to get close to 1000 points
for _ in range(17):
self.itempool.append(get_item_to_add(weights, extra_points_added, multipliers_added, items_added))
score_in_logic = dice_simulation_fill_pool(
self.itempool + self.precollected,
self.frags_per_dice,
self.frags_per_roll,
self.possible_categories,
self.difficulty,
self.player,
)
# if we overshoot, remove items until you get below 1000, then return the last removed item
if score_in_logic > 1000:
removed_item = ""
while score_in_logic > 1000:
removed_item = self.itempool.pop()
score_in_logic = dice_simulation_fill_pool(
self.itempool + self.precollected,
self.frags_per_dice,
self.frags_per_roll,
self.possible_categories,
self.difficulty,
self.player,
)
self.itempool.append(removed_item)
else:
# Keep adding items until a score of 1000 is in logic
while score_in_logic < 1000:
item_to_add = get_item_to_add(weights, extra_points_added, multipliers_added, items_added)
self.itempool.append(item_to_add)
if item_to_add == "1 Point":
score_in_logic += 1
elif item_to_add == "10 Points":
score_in_logic += 10
elif item_to_add == "100 Points":
score_in_logic += 100
else:
score_in_logic = dice_simulation_fill_pool(
self.itempool + self.precollected,
self.frags_per_dice,
self.frags_per_roll,
self.possible_categories,
self.difficulty,
self.player,
)
# count the number of locations in the game.
already_items = len(self.itempool) + 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
extra_locations_needed += (already_items - 45) // 15
self.number_of_locations = already_items + extra_locations_needed
# From here, we will count the number of items in the self.itempool, and add useful/filler items to the pool,
# making sure not to exceed the number of locations.
# first, we flood the entire pool with extra points (useful), if that setting is chosen.
if self.options.add_bonus_points == AddExtraPoints.option_all_of_it: # all of the extra points
already_items = len(self.itempool) + 1
self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 100)
# second, we flood the entire pool with story chapters (filler), if that setting is chosen.
if self.options.add_story_chapters == AddStoryChapters.option_all_of_it: # all of the story chapters
already_items = len(self.itempool) + 1
number_of_items = min(self.number_of_locations - already_items, 100)
number_of_items = (number_of_items // 10) * 10 # story chapters always come in multiples of 10
self.itempool += ["Story Chapter"] * number_of_items
# add some extra points (useful)
if self.options.add_bonus_points == AddExtraPoints.option_sure: # add extra points if wanted
already_items = len(self.itempool) + 1
self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 10)
# add some story chapters (filler)
if self.options.add_story_chapters == AddStoryChapters.option_sure: # add extra points if wanted
already_items = len(self.itempool) + 1
if self.number_of_locations - already_items >= 10:
self.itempool += ["Story Chapter"] * 10
# add some more extra points if there is still room
if self.options.add_bonus_points == AddExtraPoints.option_sure:
already_items = len(self.itempool) + 1
self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 10)
# add some encouragements filler-items if there is still room
already_items = len(self.itempool) + 1
self.itempool += ["Encouragement"] * min(self.number_of_locations - already_items, 5)
# add some fun facts filler-items if there is still room
already_items = len(self.itempool) + 1
self.itempool += ["Fun Fact"] * min(self.number_of_locations - already_items, 5)
# finally, add some "Good RNG" and "Bad RNG" items to complete the item pool
# these items are filler and do not do anything.
# probability of Good and Bad rng, based on difficulty for fun :)
p = 1.1 - 0.25 * self.difficulty
already_items = len(self.itempool) + 1
self.itempool += self.random.choices(
["Good RNG", "Bad RNG"], weights=[p, 1 - p], k=self.number_of_locations - already_items
)
# we are done adding items. Now because of the last step, number of items should be number of locations
already_items = len(self.itempool) + 1
if already_items != self.number_of_locations:
raise Exception(
f"[Yacht Dice] Number in self.itempool is not number of locations "
f"{already_items} {self.number_of_locations}."
)
# add precollected items using push_precollected. Items in self.itempool get created in create_items
for item in self.precollected:
self.multiworld.push_precollected(self.create_item(item))
# make sure one dice and one roll is early, so that you will have 2 dice and 2 rolls soon
self.multiworld.early_items[self.player]["Dice"] = 1
self.multiworld.early_items[self.player]["Roll"] = 1
def create_items(self):
self.multiworld.itempool += [self.create_item(name) for name in self.itempool]
def create_regions(self):
# call the ini_locations function, that generates locations based on the inputs.
location_table = ini_locations(
self.goal_score,
self.max_score,
self.number_of_locations,
self.difficulty,
self.skip_early_locations,
self.multiworld.players,
)
# 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
]
# Add the victory item to the correct location.
# The website declares that the game is complete when the victory item is obtained.
victory_location_name = f"{self.goal_score} score"
self.get_location(victory_location_name).place_locked_item(self.create_item("Victory"))
# add the regions
connection = Entrance(self.player, "New Board", menu)
menu.exits.append(connection)
connection.connect(board)
self.multiworld.regions += [menu, board]
def set_rules(self):
"""
set rules per location, and add the rule for beating the game
"""
set_yacht_rules(
self.multiworld,
self.player,
self.frags_per_dice,
self.frags_per_roll,
self.possible_categories,
self.difficulty,
)
set_yacht_completion_rules(self.multiworld, self.player)
def fill_slot_data(self):
"""
make slot data, which consists of yachtdice_data, options, and some other variables.
"""
yacht_dice_data = self._get_yachtdice_data()
yacht_dice_options = self.options.as_dict(
"game_difficulty",
"score_for_last_check",
"score_for_goal",
"number_of_dice_fragments_per_dice",
"number_of_roll_fragments_per_roll",
"which_story",
"allow_manual_input",
)
slot_data = {**yacht_dice_data, **yacht_dice_options} # combine the two
slot_data["number_of_dice_fragments_per_dice"] = self.frags_per_dice
slot_data["number_of_roll_fragments_per_roll"] = self.frags_per_roll
slot_data["goal_score"] = self.goal_score
slot_data["last_check_score"] = self.max_score
slot_data["allowed_categories"] = self.possible_categories
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
# 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:
change = super().collect(state, item)
if change:
state.prog_items[self.player]["state_is_fresh"] = 0
return change
def remove(self, state: CollectionState, item: Item) -> bool:
change = super().remove(state, item)
if change:
state.prog_items[self.player]["state_is_fresh"] = 0
return change

View File

@ -0,0 +1,15 @@
# Yacht Dice
Welcome to Yacht Dice, the ultimate dice-rolling adventure in Archipelago! Cast your dice, chase high scores, and unlock valuable treasures. Discover new dice, extra rolls, multipliers, and special scoring categories to enhance your game. Roll your way to victory by reaching the target score!
## Understanding Location Checks
In Yacht Dice, location checks happen when you hit certain scores for the first time. The target score for your next location check is always displayed on the website.
## Items and Their Effects
When you receive an item, it could be extra dice, extra rolls, score multipliers, or new scoring categories. These boosts help you sail towards higher scores and more loot. Other items include extra points, lore, and fun facts to enrich your journey.
## Winning the Game
Victory in Yacht Dice is all about reaching the target score. You can set your own target score, which is displayed on the website. Once you hit it, you've conquered the game!
## How to Access Options
Need to tweak your game? Head over to the [player options page](../player-options) for all your configuration options and to export your config file.

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/ArchipelagoYachtDice/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.
Both options have 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 Options Page](../player-options).
- 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).