forked from mirror/Archipelago
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
326 lines
13 KiB
Python
326 lines
13 KiB
Python
"""Functions and data for setting and calculating prices."""
|
|
|
|
from randomizer.Enums.Items import Items
|
|
from randomizer.Enums.Kongs import Kongs
|
|
from randomizer.Enums.Locations import Locations
|
|
from randomizer.Enums.Settings import RandomPrices
|
|
from randomizer.Enums.Types import Types
|
|
from randomizer.Lists.Item import ItemList
|
|
from randomizer.Lists.Location import (
|
|
ChunkyMoveLocations,
|
|
DiddyMoveLocations,
|
|
DonkeyMoveLocations,
|
|
LankyMoveLocations,
|
|
SharedMoveLocations,
|
|
TinyMoveLocations,
|
|
TrainingBarrelLocations,
|
|
)
|
|
|
|
VanillaPrices = {
|
|
Items.Vines: 0,
|
|
Items.Swim: 0,
|
|
Items.Barrels: 0,
|
|
Items.Oranges: 0,
|
|
Items.Climbing: 0,
|
|
Items.Camera: 0,
|
|
Items.Shockwave: 0,
|
|
Items.CameraAndShockwave: 0,
|
|
Items.BaboonBlast: 3,
|
|
Items.StrongKong: 5,
|
|
Items.GorillaGrab: 7,
|
|
Items.ChimpyCharge: 3,
|
|
Items.RocketbarrelBoost: 5,
|
|
Items.SimianSpring: 7,
|
|
Items.Orangstand: 3,
|
|
Items.BaboonBalloon: 5,
|
|
Items.OrangstandSprint: 7,
|
|
Items.MiniMonkey: 3,
|
|
Items.PonyTailTwirl: 5,
|
|
Items.Monkeyport: 7,
|
|
Items.HunkyChunky: 3,
|
|
Items.PrimatePunch: 5,
|
|
Items.GorillaGone: 7,
|
|
Items.Coconut: 3,
|
|
Items.Peanut: 3,
|
|
Items.Grape: 3,
|
|
Items.Feather: 3,
|
|
Items.Pineapple: 3,
|
|
Items.HomingAmmo: 5,
|
|
Items.SniperSight: 7,
|
|
Items.Bongos: 3,
|
|
Items.Guitar: 3,
|
|
Items.Trombone: 3,
|
|
Items.Saxophone: 3,
|
|
Items.Triangle: 3,
|
|
Items.ProgressiveSlam: [0, 5, 7],
|
|
Items.ProgressiveAmmoBelt: [3, 5],
|
|
Items.ProgressiveInstrumentUpgrade: [5, 7, 9],
|
|
}
|
|
|
|
ProgressiveMoves = {Items.ProgressiveSlam: 3, Items.ProgressiveAmmoBelt: 2, Items.ProgressiveInstrumentUpgrade: 3}
|
|
|
|
|
|
def CompleteVanillaPrices():
|
|
"""Complete the list of Vanilla prices with non-move items needing to cost 0."""
|
|
for item_id, item in ItemList.items():
|
|
if item_id not in VanillaPrices.keys():
|
|
VanillaPrices[item_id] = 0
|
|
|
|
|
|
def GetPriceWeights(weight):
|
|
"""Get the parameters for the price distribution."""
|
|
# Each kong can buy up to 14 items
|
|
# Vanilla: Can spend up to 74 coins, avg. price per item 5.2857
|
|
# Low: 1-4 coins most of the time
|
|
# Medium: 1-8 coins most of the time
|
|
# High: 1-12 coins (cannot be greater than 12)
|
|
# Extreme: Average of 11, can be up to 15, requires starting with Shockwave
|
|
# Free: All moves are zero coins
|
|
avg = 4.5
|
|
stddev = 2
|
|
upperLimit = 9
|
|
if weight == RandomPrices.high:
|
|
avg = 6.5
|
|
stddev = 3
|
|
upperLimit = 12
|
|
elif weight == RandomPrices.low:
|
|
avg = 2.5
|
|
stddev = 1
|
|
upperLimit = 6
|
|
elif weight == RandomPrices.extreme:
|
|
avg = 11
|
|
stddev = 2
|
|
upperLimit = 15
|
|
return (avg, stddev, upperLimit)
|
|
|
|
|
|
def RandomizePrices(spoiler, weight):
|
|
"""Generate randomized prices for each shop location."""
|
|
prices = {}
|
|
parameters = GetPriceWeights(weight)
|
|
shopLocations = [location_id for location_id, location in spoiler.LocationList.items() if location.type == Types.Shop]
|
|
for location in shopLocations:
|
|
prices[location] = GenerateRandomPrice(spoiler.settings.random, weight, parameters[0], parameters[1], parameters[2])
|
|
# Progressive items get their own price pool
|
|
for item in ProgressiveMoves.keys():
|
|
prices[item] = []
|
|
for i in range(ProgressiveMoves[item]):
|
|
prices[item].append(GenerateRandomPrice(spoiler.settings.random, weight, parameters[0], parameters[1], parameters[2]))
|
|
return prices
|
|
|
|
|
|
def GenerateRandomPrice(rando, weight, avg, stddev, upperLimit):
|
|
"""Generate a random price to assign."""
|
|
lowerLimit = 1
|
|
if weight == RandomPrices.free:
|
|
newPrice = 0
|
|
else:
|
|
newPrice = round(rando.normalvariate(avg, stddev))
|
|
if newPrice < lowerLimit:
|
|
newPrice = lowerLimit
|
|
elif newPrice > upperLimit:
|
|
newPrice = upperLimit
|
|
return newPrice
|
|
|
|
|
|
def GetMaxForKong(spoiler, kong):
|
|
"""Get the maximum amount of coins the given kong can spend."""
|
|
# Track shared moves specifically because their prices are stored specially
|
|
settings = spoiler.settings
|
|
found_slams = 0
|
|
found_instrument_upgrades = 0
|
|
found_ammo_belts = 0
|
|
total_price = 0
|
|
# Look for moves placed in shared move locations that have prices
|
|
paidSharedMoveLocations = SharedMoveLocations - TrainingBarrelLocations - {Locations.CameraAndShockwave}
|
|
for location in paidSharedMoveLocations:
|
|
item_id = spoiler.LocationList[location].item
|
|
if item_id is not None and item_id != Items.NoItem:
|
|
if item_id == Items.ProgressiveSlam:
|
|
total_price += settings.prices[item_id][found_slams]
|
|
found_slams += 1
|
|
elif item_id == Items.ProgressiveInstrumentUpgrade:
|
|
total_price += settings.prices[item_id][found_instrument_upgrades]
|
|
found_instrument_upgrades += 1
|
|
elif item_id == Items.ProgressiveAmmoBelt:
|
|
total_price += settings.prices[item_id][found_ammo_belts]
|
|
found_ammo_belts += 1
|
|
# Vanilla prices are by item, not by location
|
|
elif settings.random_prices == RandomPrices.vanilla:
|
|
total_price += settings.prices[item_id]
|
|
else:
|
|
total_price += settings.prices[location]
|
|
|
|
kongMoveLocations = DiddyMoveLocations.copy()
|
|
if kong == Kongs.donkey:
|
|
kongMoveLocations = DonkeyMoveLocations.copy()
|
|
total_price += 2 # For Arcade round 2
|
|
elif kong == Kongs.lanky:
|
|
kongMoveLocations = LankyMoveLocations.copy()
|
|
elif kong == Kongs.tiny:
|
|
kongMoveLocations = TinyMoveLocations.copy()
|
|
kongMoveLocations.remove(Locations.CameraAndShockwave)
|
|
elif kong == Kongs.chunky:
|
|
kongMoveLocations = ChunkyMoveLocations.copy()
|
|
|
|
for location in kongMoveLocations:
|
|
if spoiler.LocationList[location].inaccessible: # Ignore any shop locations that don't even exist anymore
|
|
continue
|
|
item_id = spoiler.LocationList[location].item
|
|
if item_id is not None and item_id != Items.NoItem:
|
|
if item_id == Items.ProgressiveSlam:
|
|
total_price += settings.prices[item_id][found_slams]
|
|
found_slams += 1
|
|
elif item_id == Items.ProgressiveInstrumentUpgrade:
|
|
total_price += settings.prices[item_id][found_instrument_upgrades]
|
|
found_instrument_upgrades += 1
|
|
elif item_id == Items.ProgressiveAmmoBelt:
|
|
total_price += settings.prices[item_id][found_ammo_belts]
|
|
found_ammo_belts += 1
|
|
# Vanilla prices are by item, not by location
|
|
elif settings.random_prices == RandomPrices.vanilla:
|
|
total_price += settings.prices[item_id]
|
|
else:
|
|
total_price += settings.prices[location]
|
|
return total_price
|
|
|
|
|
|
SlamProgressiveSequence = [Locations.SuperSimianSlam, Locations.SuperDuperSimianSlam]
|
|
FunkySequence = [
|
|
[Locations.CoconutGun, Locations.PeanutGun, Locations.GrapeGun, Locations.FeatherGun, Locations.PineappleGun],
|
|
Locations.AmmoBelt1,
|
|
Locations.HomingAmmo,
|
|
Locations.AmmoBelt2,
|
|
Locations.SniperSight,
|
|
]
|
|
CandySequence = [
|
|
[Locations.Bongos, Locations.Guitar, Locations.Trombone, Locations.Saxophone, Locations.Triangle],
|
|
Locations.MusicUpgrade1,
|
|
Locations.ThirdMelon,
|
|
Locations.MusicUpgrade2,
|
|
]
|
|
DonkeySequence = [Locations.BaboonBlast, Locations.StrongKong, Locations.GorillaGrab]
|
|
DiddySequence = [Locations.ChimpyCharge, Locations.RocketbarrelBoost, Locations.SimianSpring]
|
|
LankySequence = [Locations.Orangstand, Locations.BaboonBalloon, Locations.OrangstandSprint]
|
|
TinySequence = [Locations.MiniMonkey, Locations.PonyTailTwirl, Locations.Monkeyport]
|
|
ChunkySequence = [Locations.HunkyChunky, Locations.PrimatePunch, Locations.GorillaGone]
|
|
Sequences = [
|
|
SlamProgressiveSequence,
|
|
FunkySequence,
|
|
CandySequence,
|
|
DonkeySequence,
|
|
DiddySequence,
|
|
LankySequence,
|
|
TinySequence,
|
|
ChunkySequence,
|
|
]
|
|
|
|
"""
|
|
So for coin logic, we want to make sure the player can't spend coins incorrectly and lock themselves out.
|
|
This means every buyable item has to account for, potentially, buying every other possible item first.
|
|
So each price will be inflated by a lot for logic purposes.
|
|
Total prices are as follows, in vanilla:
|
|
Cranky generic: 12
|
|
Cranky specific: 15
|
|
Candy generic: 21
|
|
Candy specific: 3
|
|
Funky generic: 20
|
|
Funky specific: 3
|
|
Total one kong can possibly spend: 74
|
|
|
|
The following only applies if move locations are not decoupled, meaning certain locations must be bought in sequence:
|
|
So basically, whatever "line" the kong is buying from, need to subtract prices
|
|
from future entries in that line from 74 (or whatever the max is if prices are random).
|
|
So since Cranky's upgrades cost 3, 5, and 7, the logical price of his
|
|
first upgrade will be 74 - 7 - 5 = 62.
|
|
Since prices can be randomized, we will dynamically subtract the prices of future purchases
|
|
in any given sequence.
|
|
|
|
If moves are decoupled so that they don't need be bought in sequence, then any location could be the final location,
|
|
meaning we just must consider the maximum price for every location.
|
|
"""
|
|
|
|
|
|
def GetPriceAtLocation(settings, location_id, location, slamLevel, ammoBelts, instUpgrades):
|
|
"""Get the price at this location."""
|
|
item = location.item
|
|
# Progressive items have their prices managed separately
|
|
if item == Items.ProgressiveSlam:
|
|
if slamLevel in [1, 2]:
|
|
return settings.prices[item][slamLevel - 1]
|
|
else:
|
|
# If already have max slam, there's no move to buy (this is fine only if it's in VerifyWorld)
|
|
return 0
|
|
elif item == Items.ProgressiveAmmoBelt:
|
|
if ammoBelts in [0, 1]:
|
|
return settings.prices[item][ammoBelts]
|
|
else:
|
|
# If already have max ammo belt, there's no move to buy (this shouldn't happen?)
|
|
return 0
|
|
elif item == Items.ProgressiveInstrumentUpgrade:
|
|
if instUpgrades in [0, 1, 2]:
|
|
return settings.prices[item][instUpgrades]
|
|
else:
|
|
# If already have max instrument upgrade, there's no move to buy (this shouldn't happen?)
|
|
return 0
|
|
# Vanilla prices are by item, not by location
|
|
elif settings.random_prices == RandomPrices.vanilla:
|
|
# Treat the location as free if it's empty
|
|
if item is None or item == Items.NoItem:
|
|
return 0
|
|
return settings.prices[item]
|
|
# In all other cases, the price is determined solely by the location
|
|
return settings.prices[location_id]
|
|
|
|
|
|
def KongCanBuy(spoiler, location_id, logic, kong, buy_empty=False):
|
|
"""Check if given kong can logically purchase the specified location."""
|
|
location = spoiler.LocationList[location_id]
|
|
# If nothing is sold here, return true
|
|
if not buy_empty and (location.item is None or location.item == Items.NoItem):
|
|
return True
|
|
price = GetPriceAtLocation(logic.settings, location_id, location, logic.Slam, logic.AmmoBelts, logic.InstUpgrades)
|
|
|
|
# Simple price check - combination of purchases will be considered outside this method
|
|
if price is not None:
|
|
# print("KongCanBuy checking item: " + str(LocationList[location].item))
|
|
# print("for kong: " + kong.name + " with " + str(coins[kong]) + " coins")
|
|
# print("has price: " + str(price))
|
|
return logic.GetCoins(kong) >= price
|
|
else:
|
|
return False
|
|
|
|
|
|
def AnyKongCanBuy(spoiler, location, logic, buy_empty=False):
|
|
"""Check if any owned kong can logically purchase this location."""
|
|
return any(KongCanBuy(spoiler, location, logic, kong, buy_empty) for kong in logic.GetKongs())
|
|
|
|
|
|
def EveryKongCanBuy(spoiler, location, logic):
|
|
"""Check if any kong can logically purchase this location."""
|
|
return all(KongCanBuy(spoiler, location, logic, kong) for kong in [Kongs.donkey, Kongs.diddy, Kongs.lanky, Kongs.tiny, Kongs.chunky])
|
|
|
|
|
|
def CanBuy(spoiler, location, logic, buy_empty=False):
|
|
"""Check if an appropriate kong can logically purchase this location."""
|
|
# If we're assuming infinite coins, we can always acquire the item
|
|
if logic.assumeInfiniteCoins:
|
|
return True
|
|
# If it's in a location that doesn't care about prices, it's free!
|
|
if location in TrainingBarrelLocations or location == Locations.CameraAndShockwave:
|
|
return True
|
|
# If this is a shared location, check if the current Kong can buy the location
|
|
if location in SharedMoveLocations:
|
|
return KongCanBuy(spoiler, location, logic, logic.kong, buy_empty)
|
|
# Else a specific kong is required to buy it, so check that kong has enough coins
|
|
elif location in DonkeyMoveLocations:
|
|
return KongCanBuy(spoiler, location, logic, Kongs.donkey, buy_empty)
|
|
elif location in DiddyMoveLocations:
|
|
return KongCanBuy(spoiler, location, logic, Kongs.diddy, buy_empty)
|
|
elif location in LankyMoveLocations:
|
|
return KongCanBuy(spoiler, location, logic, Kongs.lanky, buy_empty)
|
|
elif location in TinyMoveLocations:
|
|
return KongCanBuy(spoiler, location, logic, Kongs.tiny, buy_empty)
|
|
elif location in ChunkyMoveLocations:
|
|
return KongCanBuy(spoiler, location, logic, Kongs.chunky, buy_empty)
|