Skip to content

Instantly share code, notes, and snippets.

@Corvimae
Last active October 9, 2023 17:44
Show Gist options
  • Save Corvimae/9ab80702fda7671ddbb6d8de88b7f65d to your computer and use it in GitHub Desktop.
Save Corvimae/9ab80702fda7671ddbb6d8de88b7f65d to your computer and use it in GitHub Desktop.
Cassette Beasts RNG exploration

Cassette Beasts RNG exploration script

Last updated for Cassette Beasts version 1.2.0

  1. Decompile the game and import it into Godot 3.5.1, as explained in the mod developer guide
  2. Create a new script named GenTools.gd and paste the GenTools source code into it.
  3. Create a new scene and name it seed_generator.tscn.
  4. Right click the scene and set it as the main scene.
  5. Create a new script for the scene and paste the script below into it.
  6. Run the game (F5).

You can set the range of seeds generated by editing line 10.

Async Version (Recommended)

This version outputs results as it runs and shows the current seed.

Replace the source of your scene with seed_generator.tscn. Make sure your script is named SeedGeneratorAsync.gd.

Manual setup

Set up the same as the synchronous version, but add a VBoxContainer named SeedResults with two child Labels: CurrentSeedLabel and SeedsWithResultsLabel. Set the top margin on SeedResults to 200 to avoid colliding with the speedrun timer, if enabled.

Use SeedGeneratorAsync.gd instead of SeedGenerator.gd.

Multithreaded Version (Beta)

Same as the async version, but chest contents are generated in parallel. Set up this version the same way as the async version, but use the contents as SeedGeneratorMultithreaded.gd instead.

Notes

Right now, the script below is searching for seeds that have:

  1. Custom Starter in the Harbourtown station chest
  2. X% chance to use Spit or Smack on Sirenade's Spit sticker

You should be able to modify it for your own needs pretty easily. Note that the order in which partner tapes are generated does matter, so if you comment out some of the partner tape generation you need to make sure you comment out all the tape generation lines after it as well or else it'll be wrong.

When you run the script, the splash screen will appear until the script is complete, upon which the debugger will show the results and a window with an empty screen will open.

Caveats

  • generate_chest does not currently handle chests with a set item field.
  • RNG is seeded differently when running from the debugger vs the actual game due to a decompilation error. This doesn't affect the script, but it does mean if you are trying out your seeds you need to try them in the packaged version of the game.
extends Node
class_name GenTools
static func gen_chest(loot_table: Resource, seed_value: int, key: String, loot_value: int, force_rarity_upgrade = false):
var seed_final = key.hash() ^ seed_value
seed_final = Random.new(seed_final).rand_int()
var rand = Random.new(seed_final)
var result = loot_table.generate_rewards(rand, loot_value)
if force_rarity_upgrade:
for i in range(result.size()):
result[i].item = ItemFactory.upgrade_rarity(result[i].item, rand)
return result
static func gen_character(character: Resource):
var beast = character.tapes[0]
beast.stat_increments.clear()
beast.grade = 0
beast.assign_initial_stickers(true)
return beast
static func print_chest(results: Array, tag = ""):
for item in results:
print(tag, item.amount, "x ", item.item.name)
static func print_sticker(item: StickerItem, tag = ""):
var attribute_str = ""
for attribute in item.attributes:
attribute_str += attribute.get_description(item.battle_move) + ", "
print(tag, item.name, " - ", attribute_str)
static func does_sticker_contain_attribute(sticker: Resource, values: Array):
if sticker.rarity == 0:
return false
for attribute in sticker.attributes:
var description = attribute.get_description(sticker.battle_move)
for value in values:
if value in description && (description.begins_with("+9") || description.begins_with("+8") || description.begins_with("+7")):
return true
return false
static func does_chest_contain_sticker(loot: Array, move: String):
for item in loot:
if move in item.item.name:
return true
return false
static func gen_tape(seed_value: int, form, bootleg_rate: int, seed_key: String):
var rand = Random.new(Random.child_seed(seed_value, seed_key))
var tape = MonsterTape.new()
# if form_bb_key != "" and has_bb(form_bb_key):
# form = get_bb(form_bb_key)
if form is MonsterSpawnProfile:
form = form.choose_form(rand)
assert (form is MonsterForm)
if not (form is MonsterForm):
return false
# if use_randomiser_mapping:
# form = MonsterForms.get_species_mapping(form)
tape.form = form
if rand.rand_bool(bootleg_rate):
var ElementalTypes = Datatables.load("res://data/elemental_types/")
var type = rand.choice(ElementalTypes.table.values())
assert (type is ElementalType)
tape.type_override = [type]
# for seed generation, don't assign initial stickers as it can mess up partner gen
# tape.assign_initial_stickers(true)
# tape.favorite = favorite
return tape
static func print_tape(tape):
print(tape.form.name, " - ", tape.type_override)
static func is_matching_bootleg(tape, species_name: String, type: String):
var has_matching_type = false
for override in tape.type_override:
if override.name == type:
has_matching_type = true
return tape.form.name == species_name && has_matching_type
[gd_scene load_steps=2 format=2]
[ext_resource path="res://SeedGeneratorAsync.gd" type="Script" id=1]
[node name="Node2D" type="Node2D"]
script = ExtResource( 1 )
[node name="SeedResults" type="VBoxContainer" parent="."]
margin_top = 200.0
margin_right = 40.0
margin_bottom = 40.0
[node name="CurrentSeedLabel" type="Label" parent="SeedResults"]
margin_right = 40.0
margin_bottom = 14.0
[node name="SeedsWithResultsLabel" type="Label" parent="SeedResults"]
margin_top = 18.0
margin_right = 40.0
margin_bottom = 32.0
extends Node2D
var DISALLOWED_MIRROR_LOCATIONS = ["CLUE_OVERWORLD_-4_-1", "CLUE_OVERWORLD_-6_-3", "CLUE_OVERWORLD_0_-6", "CLUE_OVERWORLD_1_-6", "CLUE_OVERWORLD_7_-2"]
func _ready():
BattleMoves.setup()
var locations = Datatables.load("res://data/story_end_locations").table.values()
for n in range(5000000, 6000000):
if n % 100000 == 0:
print("------", n, "------")
var archangel_rnd = Random.new(Random.child_seed(n, "story_end_location"))
var arch_location = archangel_rnd.choice(locations).clue_location
#if arch_location in DISALLOWED_MIRROR_LOCATIONS:
# continue
var harbourtown_chest = GenTools.gen_chest(preload("res://data/loot_tables/chest_station.tres"), n, "harbourtown_station_chest1", 100, false)
if !GenTools.does_chest_contain_sticker(harbourtown_chest, "Custom Starter"):
continue
var seed_value = n #str(n).hash()
var date = 0
var rand = Random.new(Random.child_seed(seed_value ^ (date * 12), "items"))
SaveState.item_rand = rand
var sirenade = GenTools.gen_character(preload("res://data/characters/kayleigh.tres"))
var katelly = GenTools.gen_character(preload("res://data/characters/meredith.tres"))
var clocksly = GenTools.gen_character(preload("res://data/characters/eugene.tres"))
var brushroom = GenTools.gen_character(preload("res://data/characters/felix.tres"))
# var spirouette = GenTools.gen_character(preload("res://data/characters/viola.tres"))
# var pombomb = GenTools.gen_character(preload("res://data/characters/dog.tres"))
var is_spit_relevant = GenTools.does_sticker_contain_attribute(sirenade.stickers[0], ["Spit", "Smack"])
if is_spit_relevant:
print(n, " (", arch_location, "): ")
for sticker in sirenade.stickers:
if sticker.rarity > 0:
GenTools.print_sticker(sticker, " Sirenade - ") #, str(n) + ": ")
for sticker in brushroom.stickers:
if sticker.rarity > 0:
GenTools.print_sticker(sticker, " Brushroom - ") #, str(n) + ": ")
GenTools.print_chest(harbourtown_chest, " Harbourtown Station Chest - ")
print("Done!!")
extends Node2D
func round_to_dec(num, digit):
return round(num * pow(10.0, digit)) / pow(10.0, digit)
const ACTION_CLUES = [
"CLUE_ACTION_ROCK",
"CLUE_ACTION_CRATE",
"CLUE_ACTION_DASH",
"CLUE_ACTION_MAGNETISM",
"CLUE_ACTION_JUMP_3",
"CLUE_ACTION_FACE_4_DIRECTIONS",
"CLUE_ACTION_NIGHT",
"CLUE_ACTION_MAP_3",
"CLUE_ACTION_ITEM_3",
]
var locations: Array
var kayleigh_loot_table = preload("res://data/loot_tables/chest_kayleigh_home.tres")
var misc_loot_table = preload("res://data/loot_tables/chest_misc.tres")
var station_loot_table = preload("res://data/loot_tables/chest_station.tres")
var kayleigh = preload("res://data/characters/kayleigh.tres")
var bootleg_house_table = preload("res://data/monster_spawn_profiles/harbourtown_bootleg_tutorial.tres")
func generate_seed(n: int):
var arch_location = Random.new(Random.child_seed(n, "story_end_location")).choice(locations).clue_location
var arch_ritual = Random.new(Random.child_seed(n, "clue_action")).choice(ACTION_CLUES)
#if arch_location in DISALLOWED_MIRROR_LOCATIONS:
# continue
var chest_kayleigh_home = GenTools.gen_chest(kayleigh_loot_table, n, "chest_kayleigh_home", 1000, true)
var harbourtown_chest = GenTools.gen_chest(preload("res://data/loot_tables/chest_station.tres"), n, "harbourtown_station_chest1", 100, false)
var overworld_1_dash1_chest = GenTools.gen_chest(misc_loot_table, n, "overworld_1_-1_chest", 100, false)
var overworld_2_dash6_chest = GenTools.gen_chest(misc_loot_table, n, "overworld_2_-6_chest", 100, false)
var chest_overworld_2_dash3 = GenTools.gen_chest(misc_loot_table, n, "chest_overworld_2_-3", 100, false)
var chest_overworld_2_dash4 = GenTools.gen_chest(misc_loot_table, n, "chest_overworld_2_-4", 100, false)
var chest_overworld_1_dash6_1 = GenTools.gen_chest(misc_loot_table, n, "overworld_-1_-6_chest_1", 100, false)
var chest_overworld_1_dash6_2 = GenTools.gen_chest(misc_loot_table, n, "overworld_-1_-6_chest_2", 100, false)
var chest_overworld_1_dash_7_1 = GenTools.gen_chest(misc_loot_table, n, "overworld_-1_-7_chest_1", 100, false)
var chest_overworld_0_dash_7 = GenTools.gen_chest(misc_loot_table, n, "chest_overworld_0_-7", 100, false)
var all_loot = harbourtown_chest + overworld_1_dash1_chest + overworld_2_dash6_chest + chest_overworld_2_dash3 + chest_overworld_2_dash4 + chest_overworld_1_dash6_1 + chest_overworld_1_dash6_2 + chest_overworld_1_dash_7_1 + chest_overworld_0_dash_7
var has_all_items = GenTools.does_chest_contain_sticker(all_loot, "Custom Starter") && GenTools.does_chest_contain_sticker(all_loot, "ITEM_TAPE_PLANT_NAME") # && does_chest_contain_sticker(all_loot, "ITEM_TAPE_LIGHTNING_NAME")
if !has_all_items:
return false
var seed_value = n #str(n).hash()
var date = 0
var rand = Random.new(Random.child_seed(seed_value ^ (date * 12), "items"))
SaveState.item_rand = rand
var sirenade = GenTools.gen_character(kayleigh)
# var katelly = gen_character(preload("res://data/characters/meredith.tres"))
# var clocksly = gen_character(preload("res://data/characters/eugene.tres"))
# var brushroom = gen_character(preload("res://data/characters/felix.tres"))
var is_spit_relevant = GenTools.does_sticker_contain_attribute(sirenade.stickers[0], ["Spit", "Smack"])
if is_spit_relevant:
print(n, " (", arch_location, " - ", arch_ritual, "): ")
for sticker in sirenade.stickers:
if sticker.rarity > 0:
GenTools.print_sticker(sticker, " Sirenade - ") #, str(n) + ": ")
# for sticker in brushroom.stickers:
# if sticker.rarity > 0:
# print_sticker(sticker, " Brushroom - ") #, str(n) + ": ")
GenTools.print_chest(harbourtown_chest, " Harbourtown Station Chest - ")
GenTools.print_chest(overworld_1_dash1_chest, " Overworld 1, -1 Chest - ")
GenTools.print_chest(overworld_2_dash6_chest, " Overworld 2, -6 Chest - ")
GenTools.print_chest(chest_overworld_2_dash3, " Overworld 2, -3 Chest - ")
GenTools.print_chest(chest_overworld_2_dash4, " Overworld 2, -4 Chest - ")
GenTools.print_chest(chest_overworld_1_dash6_1, " Overworld -1, -6 Chest 1 - ")
GenTools.print_chest(chest_overworld_1_dash6_2, " Overworld -1, -6 Chest 2 - ")
GenTools.print_chest(chest_overworld_1_dash_7_1, " Overworld -1, -7 Chest 1 - ")
GenTools.print_chest(chest_overworld_0_dash_7, " Overworld 0, -7 Chest - ")
GenTools.print_chest(chest_kayleigh_home, " Kayleigh Chest - ")
# print_sticker(kayleigh_sticker, " Kayleigh Sticker - ")
return true
return false
var BATCH_SIZE = 500
var START_SEED = 1
var END_SEED = 10000000
var DISALLOWED_MIRROR_LOCATIONS = ["CLUE_OVERWORLD_-4_-1", "CLUE_OVERWORLD_-6_-3", "CLUE_OVERWORLD_0_-6", "CLUE_OVERWORLD_1_-6", "CLUE_OVERWORLD_7_-2"]
var current_seed: int
var has_alerted_end = false
var seeds_with_results = []
func _ready():
BattleMoves.setup()
locations = Datatables.load("res://data/story_end_locations").table.values()
current_seed = START_SEED
func _process(delta: float):
if current_seed <= END_SEED:
for n in BATCH_SIZE:
if generate_seed(current_seed):
seeds_with_results.push_front(str(current_seed))
$SeedResults/SeedsWithResultsLabel.text = "Valid seeds: " + PoolStringArray(seeds_with_results).join(", ")
current_seed += 1;
var progress_pct = round_to_dec(((current_seed - START_SEED) as float / (END_SEED - START_SEED) as float) * 100, 2)
$SeedResults/CurrentSeedLabel.text = "Current seed: " + str(current_seed) + " (" + str(progress_pct) + "%)"
else:
if !has_alerted_end:
print("Done!!!")
has_alerted_end = true
extends Node2D
func round_to_dec(num, digit):
return round(num * pow(10.0, digit)) / pow(10.0, digit)
const ACTION_CLUES = [
"CLUE_ACTION_ROCK",
"CLUE_ACTION_CRATE",
"CLUE_ACTION_DASH",
"CLUE_ACTION_MAGNETISM",
"CLUE_ACTION_JUMP_3",
"CLUE_ACTION_FACE_4_DIRECTIONS",
"CLUE_ACTION_NIGHT",
"CLUE_ACTION_MAP_3",
"CLUE_ACTION_ITEM_3",
]
var locations: Array
var kayleigh_loot_table = preload("res://data/loot_tables/chest_kayleigh_home.tres")
var misc_loot_table = preload("res://data/loot_tables/chest_misc.tres")
var station_loot_table = preload("res://data/loot_tables/chest_station.tres")
var kayleigh = preload("res://data/characters/kayleigh.tres")
var bootleg_house_table = preload("res://data/monster_spawn_profiles/harbourtown_bootleg_tutorial.tres")
func generate_all_chests(n: int):
var keys = CHESTS_TO_GENERATE.keys()
var values = CHESTS_TO_GENERATE.values()
var output = {}
for index in CHESTS_TO_GENERATE.size():
var chest_def = values[index]
var chest = GenTools.gen_chest(
chest_def.get("loot_table", misc_loot_table),
n,
chest_def.get("key"),
chest_def.get("loot_value", 100),
chest_def.get("force_rarity_upgrade", false)
)
output[keys[index]] = chest
return output
func test_items(n: int):
var results = generate_all_chests(n)
var all_loot = []
for result in results.values():
all_loot += result
var has_all_items = GenTools.does_chest_contain_sticker(all_loot, "Custom Starter") && GenTools.does_chest_contain_sticker(all_loot, "ITEM_TAPE_PLANT_NAME") # && does_chest_contain_sticker(all_loot, "ITEM_TAPE_LIGHTNING_NAME")
return has_all_items
func generate_seed(n: int):
var arch_location = Random.new(Random.child_seed(n, "story_end_location")).choice(locations).clue_location
var arch_ritual = Random.new(Random.child_seed(n, "clue_action")).choice(ACTION_CLUES)
#if arch_location in DISALLOWED_MIRROR_LOCATIONS:
# continue
var seed_value = n #str(n).hash()
var date = 0
var rand = Random.new(Random.child_seed(seed_value ^ (date * 12), "items"))
SaveState.item_rand = rand
var sirenade = GenTools.gen_character(kayleigh)
# var katelly = gen_character(preload("res://data/characters/meredith.tres"))
# var clocksly = gen_character(preload("res://data/characters/eugene.tres"))
# var brushroom = gen_character(preload("res://data/characters/felix.tres"))
var is_spit_relevant = GenTools.does_sticker_contain_attribute(sirenade.stickers[0], ["Spit", "Smack"])
if !is_spit_relevant:
return false
print(n, " (", arch_location, " - ", arch_ritual, "): ")
for sticker in sirenade.stickers:
if sticker.rarity > 0:
GenTools.print_sticker(sticker, " Sirenade - ") #, str(n) + ": ")
# for sticker in brushroom.stickers:
# if sticker.rarity > 0:
# print_sticker(sticker, " Brushroom - ") #, str(n) + ": ")
var chest_results = generate_all_chests(n)
var chest_result_keys = chest_results.keys()
var chest_result_values = chest_results.values()
for n in chest_results.size():
GenTools.print_chest(chest_result_values[n], " " + chest_result_keys[n] + " - ")
return true
var THREAD_COUNT = 10
var BATCH_SIZE = 1
var START_SEED = 2172531
var END_SEED = 2202532
var DISALLOWED_MIRROR_LOCATIONS = ["CLUE_OVERWORLD_-4_-1", "CLUE_OVERWORLD_-6_-3", "CLUE_OVERWORLD_0_-6", "CLUE_OVERWORLD_1_-6", "CLUE_OVERWORLD_7_-2"]
var CHESTS_TO_GENERATE = {
"Harbourtown Station Chest": { "key": "harbourtown_station_chest1", "loot_table": station_loot_table},
"Overworld 1, -1 Chest": { "key": "overworld_1_-1_chest" },
"Overworld 2, -6 Chest": { "key": "overworld_2_-6_chest" },
"Overworld 2, -3 Chest": { "key": "chest_overworld_2_-3" },
"Overworld 2, -4 Chest": { "key": "chest_overworld_2_-4" },
"Overworld -1, -6 Chest 1": { "key": "overworld_-1_-6_chest_1" },
"Overworld -1, -6 Chest 2": { "key": "overworld_-1_-6_chest_2" },
"Overworld -1, -7 Chest 1": { "key": "overworld_-1_-7_chest_1" },
"Overworld 0, -7 Chest": { "key": "chest_overworld_0_-7" },
"Kayleigh Chest": { "key": "chest_kayleigh_home", "loot_table": kayleigh_loot_table, "force_rarity_upgrade": true, "loot_value": 1000 }
}
var current_seed: int
var has_alerted_end = false
var seeds_with_required_items = []
var seeds_with_results = []
var threads = []
var threads_completed = 0
var start_time: int
var index_mutex: Mutex
var results_mutex: Mutex
func _ready():
BattleMoves.setup()
locations = Datatables.load("res://data/story_end_locations").table.values()
current_seed = START_SEED
start_time = OS.get_ticks_usec()
index_mutex = Mutex.new()
results_mutex = Mutex.new()
for n in THREAD_COUNT:
var thread = Thread.new()
threads.push_back(thread)
thread.start(self, "_thread_process")
func _thread_process():
while true:
index_mutex.lock()
var index = current_seed
current_seed += BATCH_SIZE
index_mutex.unlock()
if index > END_SEED:
results_mutex.lock()
threads_completed += 1
results_mutex.unlock()
break
for n in range(index, index + BATCH_SIZE):
if test_items(n):
results_mutex.lock()
seeds_with_required_items.push_front(n)
results_mutex.unlock()
func _process(delta: float):
var progress_pct = round_to_dec(((current_seed - START_SEED) as float / (END_SEED - START_SEED) as float) * 100, 2)
$SeedResults/CurrentSeedLabel.text = "Current seed: " + str(current_seed) + " (" + str(progress_pct) + "%)"
if seeds_with_required_items.size() > 0:
var possible_seed = seeds_with_required_items.pop_front()
if generate_seed(possible_seed):
seeds_with_results.push_front(str(possible_seed))
$SeedResults/SeedsWithResultsLabel.text = "Valid seeds: " + PoolStringArray(seeds_with_results).join(", ")
if threads_completed == THREAD_COUNT && seeds_with_required_items.size() == 0:
var duration = OS.get_ticks_usec() - start_time
var total_seeds = END_SEED - START_SEED
var usec_per_seed = duration / total_seeds
var seeds_per_sec = 1000000 / usec_per_seed
print("Seed generation finished in ", (duration / 1000000), " seconds (", total_seeds, " seeds tried, avg ", usec_per_seed, " usec per seed / ", seeds_per_sec, " seeds per second)")
for thread in threads:
thread.wait_to_finish()
threads.clear()
set_process(false)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment