Skip to content

Instantly share code, notes, and snippets.

@WonderGinger
Created November 2, 2024 17:39
Show Gist options
  • Save WonderGinger/8df5904f296afb8d40f879767d0ebfe1 to your computer and use it in GitHub Desktop.
Save WonderGinger/8df5904f296afb8d40f879767d0ebfe1 to your computer and use it in GitHub Desktop.
How I prepared for the 2024 celeste any% open tournament

background

the 2024 celeste any% open tournament just ended and i want to share my experience of participating, as well as some ideas i have about speedrun practice. i hope my experience helps you think about speedrunning, competition, or practice in a way you haven't before

after seeing the announcement for the tournament in late july, i decided to drop what i was doing in all red berries and give any% my all for a few months to see what i was capable of

at the time i hadn't touched any% since september 2022 with a pb of 29:41 and i knew i'd improved a lot since then so i basically had to relearn all of any%, or at least the parts that are different than in arb and 5b

my initial goal was to qualify for the division 2 bracket and improve my any% pb by a bit; i ended up qualifying for the division 1 bracket and placing 9th. afterwards i pushed for a 26 minute any% run and achieved a time of 26:49.526 on october 23, 2024

i want to share some details about how i prepared, and my experience preparing for the tournament in the months leading up to it. in particular, i used three main practice strategies:

  1. checkpoint streaks: get comfortable with a strat set by playing like i would in fullgame
  2. IL gauntlet: measure ability to play to my goal sum of best
  3. races / no-reset runs

these strategies have proven to be both effective and fun to do, so i'll share how you can try them for yourself


  • tournament bracket link: https://challonge.com/2024open_div1

  • livesplit file parsing script is below in the gist. it can be used to extract the best chapters you've done in a run, best exits, and a few other nice things (thanks to Geflüt on discord for the idea and starter code)

  • template for the spreadsheet i used: celeste progress template

spreadsheet template

first month: strat choice and streak blackout

i considered two main things when choosing an any% strat set for the tournament:

  1. "i have only 2 months to prepare for the open stage"
  2. "i will definitely go back to playing all red berries eventually as my main category"

strat choice and checkpoint baselines

given these two considerations, i knew i would have to pick strats i was already capable of playing consistently. this meant that i deliberately chose to drop fast strats that I was capable of executing if they would require grinding to become fullgame viable for me. i don't usually drop hard strats so quickly, but in a tournament-prep mindset it made sense to favor consistency to avoid spending too much time practicing individual strats

however i did tolerate some harder strats if they were also relevant in arb

i spent 2 weeks building and refining a strat set by looking at other runs and trying things out in full checkpoints. in these weeks i also recorded baseline checkpoint times so that i could choose goal times

streaks

the next 2 weeks were spent doing checkpoint streaks. "streaks" are when i play a checkpoint repeatedly until i finish enough runs in a row (usually 5) that are faster than my goal time

in order to improve my consistency across the board, i did a specific streak exercise. the important feature of this streak exercise is that the amount of time spent in each checkpoint is limited based on its length because less time should be spent on shorter segments

  • i used my checkpoint goal times to determine how long to spend on streak attempts
  • for example, in city's crossing checkpoint my goal time was 24.000, so i set a timer for 24 minutes
  • i logged in my spreadsheet the best streak i achieved in those 24 minutes and moved on to the next one
  • if i got a streak of 5+ before the timer ended, then i would move on

image

this gave me a clear idea of which checkpoints to focus on. i would do a combination of re-evaluating my goal time, relaxing my strat choices, and spending some extra time practicing each checkpoint where i failed to get a streak of 5

even without this streak exercise, streaks are generally an ideal way to practice shorter segments of a category. they forced me to play like i should in fullgame, as opposed to greeding movement and playing in a risky way with nothing to lose like i would in a checkpoint or IL pb grind

second month: IL gauntlets and races

as the tournament drew closer, i decided to do more fullgame-focused practice. for me, the best and most fun fullgame practice has been the IL gauntlet

i also mixed in some races for fun, especially as the tournament drew closer. i even did a couple of "mock ctt" (celeste time trials) sessions with other runners, where we spent 2 hours trying to get the best run we could and get used to the open ctt format

IL gauntlet

before starting, i add up all my checkpoint goals to get full chapter goals (filetime added)

image

to start, i like to do a no-reset run to establish a baseline for each chapter to populate the IL gauntlet column. then, i typically run the weakest (red) chapters first. if i improve my session IL pb without beating the goal, i usually switch to the next weakest chapter

the objective is to turn everything green, i.e. finish an IL within the sum of my checkpoint goals for every chapter

while i don't always succeed (especially at the beginning), i always see measurable improvements in my IL gauntlet performance over time. after i'm done with the session (successful or not), i log it in my spreadsheet to easily track my progress

image

the metrics i look for are, in order of importance:

  • how many chapters i succeed in
  • how long it takes
  • how fast the sum of times are (TOTALS)

compared to simply playing a chapter for a better pb, IL gauntlet successes are more closely tied to fullgame success because the best way to complete an IL gauntlet quickly is to play the same way i would in fullgame

these metrics are more useful to me for measuring progress over time than my ability to PB checkpoints or ILs; they strike a good balance between spending time playing for PB and practicing shorter segments

as the end of the second month drew near, i was already feeling pretty good. i had already gotten a few any% pbs during races, and i was confident in my gameplay

i managed to qualify for the division 1 bracket with a time of 27:17.865 on day 1 of the ctt, 14th place overall

month 3: the bracket, and the 26 grind

practice during month 3 was much simpler. i focused practice that would directly prepare me for the ctt format and potential bracket races

so, i mostly did races and no-resets during this month (but i did sprinkle in 2 IL gauntlets and some pb attempt sessions). as the end of the tournament drew near, and especially after i was eliminated, i did mostly pb attempts because i realized that i wouldn't want to move on without getting a 26 minute run

thoughts on practice methodology

a lot could be said about practicing efficiently, and it's probably true that efficient practice looks slightly different for everyone. however, there are some essential elements that i think will probably be found in any efficient strategy for speedrun practice and even practice in the context of competition in general

these are the strategies i used to prepare for the tournament and improve my any% pb, and my ideas about practice that have served me well

cut the fat

the guiding principle for my practice strategy is to improve the part of the run where i'm weakest, and spend minimal time on anything else. this sounds simple, but determining which segments are most important to practice is the hard part

it's easy to go wrong here, and one way i've gone wrong in the past is playing checkpoint and IL whack-a-mole, where i play a segment with the goal of getting a PB or "clean run", which usually takes a long time. this strategy is not inherently inefficient, but in the context of practicing for a fullgame run it's not ideal

stop checkpoint/IL hopping

i noticed something was wrong this year while practicing some arb checkpoints and ILs. there was a disparity between what i thought i was practicing for and the actual results i was seeing

at some point i had to ask myself, "what category am i really playing?" if i was being honest, then i definitely would not answer fullgame. so even though i was picking my 'weakest' IL or checkpoint to play, it wasn't efficient fullgame practice (i was still having fun and got some banger checkpoint times so it's totally fine)

how i see it, there's a spectrum between two extremes of fullgame practice methods. those extremes are room grinding and fullgame spamming; the most efficient thing to do is somewhere in-between. it's important to focus precisely on what needs the most work, but it's also important to maintain the big picture of the fullgame run and not let what i'm good at slip

goals

this is the most important part for tracking progress and it's an essential part of my practice methods. these aren't conventional goals like "i want to get 26:xx any%", although that might be my starting point

for each checkpoint in any%, i thought of the weakest time that i would consider an acceptable time to get in a fullgame run. my process for choosing these times depended on the checkpoint's length and difficulty. i was very lenient with choosing goal times for longer, more difficult, or more volatile checkpoints

for example my goal for city start was 0.8 seconds slower than my checkpoint pb, which turned out to be reasonable for that checkpoint. but for harder checkpoints where i could accept a small death or medium-sized mistake such as 6b reflection, i set my goal time accordingly

image image

at first i set my goal times with this strategy while being extra lenient on the hardest checkpoints, which resulted in a low 27 level "goal sum of best"

image (filetime added)

after several successful IL gauntlets (and a fullgame PB that was faster than the goal), i tightened up the goals on some checkpoints and reduced the sum to below 27 minutes

image (filetime added)

summary

to prepare for the any% open tournament, i had to learn much of the category almost from scratch. i used fullgame-focused practice methods to determine which segments to practice, identify strats to drop, and measure my progress

i used checkpoint streaks to get comfortable with my strat set, IL gauntlets to gague my ability to play on par with my goals in an IL context, and races / no-resets to keep the big picture in mind and tie everything together

the first two of these heavily utilize setting achievable goals at the level of individual checkpoints

i'm very happy with my results, i definitely wasn't expecting to qualify for division 1, let alone go on to do a 26 minute any% run

thanks for reading, i hope you can extract some value from my quick tournament detour. i learned a lot during these 3 months and while i enjoyed my vacation in any%, i'm feeling the magnetic pull of a certain 175 red berries ......

dz4w8xde


my pb: https://www.youtube.com/watch?v=3WQo38d-88E youtube link

#!/usr/bin/python3
from sys import maxsize
import xml.etree.ElementTree as ET
import re
import os
import argparse
from datetime import timedelta
regex = re.compile(r'((?P<hours>\d+?):)?((?P<minutes>\d+?):)?((?P<seconds>(\d+.?\d+)?)$)?')
pp = False
def parse_time(time_str):
parts = regex.match(time_str)
if not parts:
return timedelta(0)
parts = parts.groupdict()
time_params = {}
for name, param in parts.items():
if param:
time_params[name] = float(param)
return timedelta(**time_params)
def segment_is_subsplit(name):
if name is None:
return 0
return string_is_subsplit(name.text)
def string_is_subsplit(name_str):
return '-' == name_str[0]
def extract_name_from_str(str):
if string_is_subsplit(str):
return str[1:]
extract_name = re.compile(r'\{?([\w\s]+)\}?')
match = extract_name.match(str)
if match:
return match.group(1)
return None
def find_best_chapters(lss):
tree = ET.parse(lss)
root = tree.getroot()
segments = root.find("Segments")
if segments is None:
return 1
split_list = []
subsplits = []
golds = {}
best_id_dict = {}
for segment in segments:
subsplits.append(segment)
if not segment_is_subsplit(segment.find("Name")):
split_list.append(subsplits)
subsplits = []
for split in split_list:
split_name = ''
best = maxsize
if len(split) > 1:
name_res = re.search('^{(?P<name>.*)}',split[-1].find("Name").text)
if name_res is not None:
split_name = name_res.groupdict()['name']
else:
split_name = split[-1].find("Name").text
time_dict = {}
for subsplit in split:
segment_history = subsplit.find("SegmentHistory")
for time in segment_history.findall("Time"):
game_time = time.find("GameTime")
if game_time is None:
continue
id = time.get("id")
time_str = game_time.text
time_f = round(parse_time(time_str).total_seconds(), 3)
if 0 == time_f:
print(id, time_str, game_time)
return 1
if id in time_dict:
time_dict[id].append(time_f)
else:
time_dict[id] = [time_f]
times = []
for id in time_dict:
if len(time_dict[id]) == len(split):
times.append(sum(time_dict[id]))
sum_id = sum(time_dict[id])
if sum_id < best:
best_id_dict[split_name] = id
best = sum_id
best = timedelta(seconds = min(times))
else:
split_name = split[0].find("Name").text
best = parse_time(split[0].find("BestSegmentTime").find("GameTime").text)
golds[split_name] = best
output_str_list = []
output_str_list.append("chapter,time")
for name in golds:
gold_row = name + "," + str(golds[name])
output_str_list.append(gold_row)
sob = timedelta(seconds = sum([golds[name].total_seconds() for name in golds]))
sum_str = "sum," + str(sob)
output_str_list.append(sum_str)
out_filename = os.path.splitext(lss)[0] + '_chapters.txt'
with open(out_filename, 'w') as f:
f.write('\n'.join(output_str_list))
if pp:
print_table(output_str_list)
else:
print('\n'.join(output_str_list))
print()
find_best_chapters_subsplits(split_list, best_id_dict)
def find_best_chapters_subsplits(split_list, best_id_dict):
chapter_gold_subsplits = {}
for split in split_list:
if len(split) > 1:
name_res = re.search('^{(?P<name>.*)}',split[-1].find("Name").text)
if name_res is not None:
split_name = name_res.groupdict()['name']
else:
split_name = split[-1].find("Name").text
for subsplit in split:
subsplit_name = subsplit.find("Name").text
segment_history = subsplit.find("SegmentHistory")
for time in segment_history.findall("Time"):
game_time = time.find("GameTime")
if game_time is None:
continue
id = time.get("id")
if best_id_dict[split_name] != id:
continue
time_str = game_time.text
time_f = round(parse_time(time_str).total_seconds(), 3)
if 0 == time_f:
return 1
ele = (subsplit_name,time_f)
if split_name in chapter_gold_subsplits:
chapter_gold_subsplits[split_name].append(ele)
else:
chapter_gold_subsplits[split_name] = [ele]
output_str_list = []
if pp:
output_str_list = []
output_list = []
max_len = 0
for chapter in chapter_gold_subsplits:
name_list = list(map(lambda x: x[0], chapter_gold_subsplits[chapter]))
time_list = list(map(lambda x: str(x[1]), chapter_gold_subsplits[chapter]))
time_list_f = list(map(lambda x: x[1], chapter_gold_subsplits[chapter]))
header_str_list = [chapter]
header_str_list.extend(name_list)
subsplit_str_list = [str(timedelta(seconds=sum(time_list_f)))]
subsplit_str_list.extend(time_list)
max_len = max(len(header_str_list), max_len)
output_list.append(header_str_list)
output_list.append(subsplit_str_list)
for s in output_list:
if len(s) < max_len:
s.extend([','] * (max_len - len(s) - 1))
output_str_list = list(map(lambda x: ','.join(x), output_list))
print_table(output_str_list, row_sep='-', row_sep_interval=2)
else:
for chapter in chapter_gold_subsplits:
output_str_list.append(chapter + ',' + ','.join(map(lambda x: str(timedelta(seconds=x[1])), chapter_gold_subsplits[chapter])))
print('\n'.join(output_str_list))
def find_best_checkpoints(lss):
tree = ET.parse(lss)
root = tree.getroot()
segments = root.find("Segments")
if segments is None:
return 1
split_names = []
pb = {}
golds = {}
for segment in segments:
name = segment.find("Name")
split_times = segment.find("SplitTimes")
if split_times is None or name is None:
continue
split_name = str(name.text)
pb_split = split_times.find("SplitTime")
if pb_split:
pb_split = pb_split.find("GameTime")
if pb_split:
pb_split = parse_time(pb_split.text)
gold_split = segment.find("BestSegmentTime")
if gold_split:
gold_split = gold_split.find("GameTime")
if gold_split:
gold_split = parse_time(gold_split.text)
split_names.append(split_name)
golds[split_name] = gold_split
pb[split_name] = pb_split
best_split_times = find_best_split_times(lss)
if not isinstance(best_split_times, dict):
return 0
output_str_list = []
output_str_list.append("Checkpoint,Golds,PB Split Times,Best Split Times")
for cp in split_names:
output_str_list.append(f"{cp},{golds[cp].text},{pb[cp].text},{str(timedelta(seconds=best_split_times[cp]))}")
out_filename = os.path.splitext(lss)[0] + '_checkpoints.txt'
with open(out_filename, 'w') as f:
f.write('\n'.join(output_str_list))
if pp:
print_table(output_str_list)
else:
print('\n'.join(output_str_list))
def find_best_split_times(lss):
tree = ET.parse(lss)
root = tree.getroot()
segments = root.find("Segments")
if segments is None:
return 1
best_split_times = {}
time_dict = {}
segment_names = []
for segment in segments:
name = segment.find("Name")
if name is None:
continue
segment_names.append(name.text)
segment_history = segment.find("SegmentHistory")
if segment_history is None:
continue
for time in segment_history.findall("Time"):
game_time = time.find("GameTime")
if game_time is None:
continue
id = time.get("id")
time_str = game_time.text
time_f = round(parse_time(time_str).total_seconds(), 3)
if 0 == time_f:
print(id, time_str, game_time)
return 1
if id in time_dict:
time_dict[id].append(time_dict[id][-1] + time_f)
else:
time_dict[id] = [time_f]
for id in time_dict:
time_list = time_dict[id]
for ii, time in enumerate(time_list):
found_best_exit = False
if segment_names[ii] in best_split_times:
if time < best_split_times[segment_names[ii]]:
found_best_exit = True
else:
found_best_exit = True
if found_best_exit:
best_split_times[segment_names[ii]] = time
return best_split_times
def find_best_exits(lss):
best_split_times = find_best_split_times(lss)
if not isinstance(best_split_times, dict):
return 0
best_exits = {extract_name_from_str(k):str(timedelta(seconds=v)) for k, v in best_split_times.items() if not string_is_subsplit(k)}
output_str_list = []
for chapter in best_exits:
output_str_list.append(str(chapter) + ',' + best_exits[chapter])
out_filename = os.path.splitext(lss)[0] + '_best_exits.txt'
with open(out_filename, 'w') as f:
f.write('\n'.join(output_str_list))
if pp:
print_table(output_str_list)
else:
print('\n'.join(output_str_list))
def print_table(rows, row_sep=None, row_sep_interval=1, padding=2):
col_cnt = str.count(rows[0], ',') + 1
max_col_w = [0] * col_cnt
token_list_2d = []
for ii, line in enumerate(rows):
tokens = str.split(line, ',')
token_list_2d.append(tokens)
for ii, t in enumerate(tokens):
max_col_w[ii] = max(len(t) + padding, max_col_w[ii])
line_count = 0
for row in token_list_2d:
if (row_sep is not None) and (line_count == row_sep_interval):
if '\n' == row_sep:
print()
print(str(row_sep * sum(max_col_w)))
line_count = 0
line_count += 1
for ii, token in enumerate(row):
print(token , end='')
print(' ' * (max_col_w[ii] - len(token)), end='')
print()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('lss', action='store', help='path to .lss file')
parser.add_argument('-pretty', action='store_true', help='pretty print the values')
args = parser.parse_args()
if args.pretty:
pp = True
print(args.lss)
print()
print("Best Checkpoints")
find_best_checkpoints(args.lss)
print()
print("Best Chapters")
find_best_chapters(args.lss)
print()
print("Best Exits")
find_best_exits(args.lss)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment