Skip to content

Hacking the Write-Up⚓︎

In an - oddly enough unlinked-to - video on Youtube, Tinsels and Templates: Putting the Fun Back in Functional Reporting, Thomas Bouve recommends a template for mkdocs for creating the HHC write-up.

Obviously I have followed his advice and used his template. It certainly made writing the report feel easier!

Still, there is some repetition and drudgery involved:

  • Putting the names of the challenges in three places (navigation in "mkdocs.yml", in index.md and in the respective objective.md file)
  • Creating the stars for the difficulty ratings - again, in index.md and in objective.md
  • Copying the conversations and hints from the game into the md files - and assigning them to the correct objective

Why not automate those tasks?

Looking where the basic information was available, I stumbled upon the web socket messages that handle most of the communication between the client side java script code and the server backend. These messages come in "types"; for this purpose, "SET_TOKEN" and "NPC_CONVOS" turned out to contain the needed data.

SET_TOKEN has the objectives, their full name ("displayName"), their difficulty level, in "content" the question to be answered, an URL for the terminal or minigame... It also holds the hints, who gives them ("sourceDisplayName"), and to which objective they apply ("appliesTo").

NPC_CONVOS contains the conversations of the non-player characters - the elves and Santa.

only at the end...

Those messages will only have objective you encountered, hints you earned and conversations you had. Only extract them at the end of playing the HHC, so they may be as complete as possible.

Both messages can be extracted from the developer console in Chrome or Firefox.

Timing

The developer console must be started before loading the Invite page. Only then is the web socket connection visible in the "Network" tab.

So, I set off to write some code that would parse both collected messages and create

  • the objectives part of the navigation menu,
  • the objectives part of index.md,
  • and skeleton md files for all the objectives it can find.

These skeleton files include

  • headings,
  • the difficulty rating,
  • the challenge text,
  • associated hints
  • and NPC conversations for that challenge.

Here is an example for the "Na'an" challenge.

The "solution" section is still empty; maybe the AI revolution will fix that.

Anyway, here is my script:

mkreport.py
mkreport.py
#!/usr/bin/python3

import json
import sys
import re



# return n solid stars + (5-n) regular stars
def get_stars(n):
    if isinstance(n,str): n=int(n)
    r=""
    for i in range(0,n):
        r+=":fontawesome-solid-star:"
    for i in range(n,5):
        r+=":fontawesome-regular-star:"
    return r


def complete2obj (c):
    # remove "Complete"
    mo=re.compile(r'^(.*)Complete$').search(c)
    if mo:
        return "obj"+(mo.group(1).title())
    return None

def find_objective (name, objectives):
    if not isinstance(name,str): return None
    if name in objectives: return objectives[name]
    comName="obj"+(name.title())
    if comName in objectives: return objectives[comName]
    return None


if len(sys.argv)!=3:
    print("Usage: mkreport SET_TOKENS NPC_CONVOS")
    sys.exit()



file_tokens=sys.argv[1]
file_convos=sys.argv[2]

with open(file_tokens) as file: content_tokens=file.read()
with open(file_convos) as file: content_convos=file.read()




parsed_tokens=json.loads(content_tokens)
parsed_convos=json.loads(content_convos)

usertokens=parsed_tokens["userTokens"]

userid=list(usertokens.keys())[0]
print(f"Userid: {userid}")

this_users_tokens=usertokens[userid]


objectives={}

# look for objectives
for elem in this_users_tokens:
    if isinstance(elem, dict):
        if "meta" in elem:
            for (name, data) in elem["meta"].items():
                if data["type"] == "objective": 
                    objectives[name]=data


# look for hints
for elem in this_users_tokens:
    if isinstance(elem, dict):
        if "meta" in elem:
            for (metaname, metadata) in elem["meta"].items():
                if metadata["type"] == "hint": 
                    applies_to=metadata.get("appliesTo")
                    if not applies_to: 
                        print(f"*** Hint {metaname}:  No 'appliesTo' field ***")
                        continue
                    objective=find_objective(applies_to, objectives)
                    if objective: 
                        print(f'Hint:  {metaname} appplied to {objective["shortName"]}')
                        if not "hints" in objective : objective["hints"]=[]
                        objective["hints"].append(metadata)
                    else:
                        print(f'*** Hint without objective:  {metaname} applies to {applies_to} ***')



# scan convos
for (fullname, dialog) in parsed_convos["npcDialog"].items():
    #elf_full_name[dialog["shortName"]]=dialog["displayName"]
    tracks=list(dialog["tracks"].items())
    for track_index in range( len(tracks)-1,0, -1):
        nameComplete=tracks[track_index][0]
        lines=tracks[track_index][1]

        obj_name=complete2obj(nameComplete)
        if obj_name:
            if obj_name in objectives:
                objective=objectives[obj_name]
                objective["elf"]=dialog["displayName"]
                objective["elf_after"]="<br/>".join(lines)
                objective["elf_before"]="<br/>".join(tracks[track_index-1][1])


objectives=dict(sorted(objectives.items(), key=lambda obj: int(obj[1]["order"])))



#----------------------------------------------------------------------
# create output
#----------------------------------------------------------------------
print("----------------------------------------------------------------------")
print("For mkdocs.yml:")
print("----------------------------------------------------------------------")
print("- Objectives:")
print("  - Catch-All-Section:")
for (name, objective) in objectives.items():
    display_name=objective["displayName"].replace(":", "")

    print(f'      - {display_name}:  \'objectives/{name}.md\'')
print()
print()
print("----------------------------------------------------------------------")
print("For docs/index.md:")
print("----------------------------------------------------------------------")
obj_index=0
for (name, objective) in objectives.items():
    obj_index+=1
    print(f'!!! success "{obj_index} {objective["displayName"]} - {get_stars(objective["difficulty"])}"')
    print(f'    This challenge does not feature an explicit answer, but will appear as "Achievement" when [solved](./objectives/{name}.md).')
    print()

print()
print()


#----------------------------------------------------------------------
# docs/objectives/NAME.md
#----------------------------------------------------------------------
for (name, objective) in objectives.items():
    elf=objective.get("elf", "UNKNOWN PERSON")
    elf_before=objective.get("elf_before", "INSERT TEXT HERE")
    elf_after=objective.get("elf_after", "INSERT TEXT HERE")

    text=f'''
---
icon: material/text-box-outline
---

# {objective["displayName"]}

**Difficulty**: {get_stars(objective["difficulty"])}<br/>
'''
    if "urlProd" in objective: text+=f'''
**Direct link**: [{objective["urlProd"]}]({objective["urlProd"]})
'''
    text+=f'''

## Objective

!!! question "Request"
    {objective["content"]}

??? quote "{elf}"
    {elf_before}

## Hints

'''
    if "hints" in objective:
        for hint in objective["hints"]:
            text+=f'''
??? tip "{hint["displayName"]}"
    From: {hint["sourceDisplayName"]}

    {hint["content"]}
'''

    text+=f'''
## Solution


### Admonitions

!!! warning "Beware of AIs"
    Their golden words might hide a sharp edge

!!! info "Palm tree lighting"
    While on the island, make sure to hang your Christmas lights on a palm tree. It’s not only festive but also a great beacon for Santa to find you!

### Images


!!! success "Answer"
    After solving the challenge, the fact will be listed as an "Achievements" in the player's badge.


## Response

!!! quote "{elf}"
    {elf_after}
'''

    print(f"Writing {name}.md")
    with open(f"{name}.md", "w") as output: output.write(text)

Run "./mkreport.py <file with content of SET_TOKENS> <file with content of NPS_CONVOS>".

Limitations

This code tries to guess certain relationships: When a NPC has a conversation "track" called "${objective}Complete", it assumes that the previous conversation pertains to the beginning of this ${objective}. The code will gladly confuse "objective" with "objObjective" when looking for a match.

The result is not perfect and will require manual edits for some challenges.

Nevertheless, it has been useful for me in getting started.