Module framework.levels

Source code
import json
import importlib
import os
import random
import hashlib
import shutil

import google.auth
from jinja2 import Template

from .config import cfg


def import_level(level_path):
    '''Returns the imported python module of the given level path.

    Parameters:
        level_path (str): Relative path of level from core/levels/ directory
    
    Returns:
        module: The python module of the given level
    '''
    # Check if level is in config
    if not level_path in cfg.get_seeds():
        exit(
            f'Level: {level_path} not found in levels list. A list of available levels can be found by running:\n'
            '  python3 thunder.py list_levels\n'
            'If this is a custom level that you have not yet imported, run:\n'
            '  python3 thunder.py add_levels [level-path]')

    level_name = os.path.basename(level_path)
    try:
        level_module = importlib.import_module(
            f'.levels.{level_path.replace("/", ".")}.{level_name}', package='core')
    except ImportError:
        raise ImportError(
            f'Cannot import level: {level_path}. Check above error for details.')
    return level_module


def add_level(level_path):
    '''Generates a seed for a new level, which is necessary to generate level secrets.

    Parameters:
        level_path (str): The path of the level
    '''
    # Check to see if level already has a seed
    if level_path in cfg.get_seeds():
        exit(f'{level_path} has already been imported.')
    # Check to see if level has the necessary files
    level_name = os.path.basename(level_path)
    level_py_path = f'core/levels/{level_path}/{level_name}.py'
    level_yaml_path = f'core/levels/{level_path}/{level_name}.yaml'
    if not os.path.exists(level_py_path):
        exit(f'Expected level python file was not found at {level_py_path}')
    if not os.path.exists(level_yaml_path):
        exit(
            f'Expected yaml configuration file was not found at {level_yaml_path}')
    # Generate a random seed for the specified level
    seeds = cfg.get_seeds()
    seeds[level_path] = str(random.randint(100000, 999999))
    cfg.set_seeds(seeds)


def make_secret(level_path, chars=None):
    '''Generates the secret of the level by hashing the level seed and the player's project id.

    Parameters:
        level_path (str): The path of the level
        chars (int, optional): Integer that sets the length of the returned secret. If not supplied, the secret will be arbitrary length based on the value of the hash.

    Returns:
        str: String that contains an integer version of the hash of the level seed and project id 
    '''
    credentials, project_id = google.auth.default()
    seeds = cfg.get_seeds()
    seed = seeds[level_path]
    if(not chars):
        return str(int(hashlib.sha1((seed+project_id).encode('utf-8')).hexdigest(), 16))
    else:
        return str(int(hashlib.sha1((seed+project_id).encode('utf-8')).hexdigest(), 16))[:chars]


def write_start_info(level_path, message, file_name=None, file_content=None):
    '''Prints the start message and saves start files.
    
    Prints the supplied start message and saves it to a text file, and saves another optional file, which can be used for credential files, ssh key files, or any other file that the player is given at the beginning of the level. This function saves files in the start/ directory.

    Parameters:
        level_path (str): The path of the level being created
        message (str): The start message that will be printed and saved
        file_name (str, optional): The name of the optional extra file
        file_content (str, optional): The contents of the optional extra file
    '''
    print('\n')
    # If start directory is not present, create it
    if not os.path.exists('start'):
        os.makedirs('start')
    # If there is an extra file, create it
    if file_name and file_content:
        file_path = f'start/{file_name}'
        with open(file_path, 'w+') as f:
            f.write(file_content)
        os.chmod(file_path, 0o400)
        print(
            f'Starting file: {file_name} has been written to {file_path}')
    # Write the start message to a file 
    level_name = os.path.basename(level_path)
    message_file_path = f'start/{level_name}.txt'
    with open(message_file_path, 'w+') as f:
        f.write(message)
    os.chmod(message_file_path, 0o400)
    print(
        f'Starting message for {level_path} has been written to {message_file_path}')
    # Print start message
    print(f'Start Message: {message}')
    print('\n')


def delete_start_files():
    '''Deletes the start files of a level. This function should be called upon level destruction.'''
    shutil.rmtree('start')


def generate_level_docs():
    '''Generates HTML documents for each level based on each level's [levelname].hints.html'''
    with open('core/framework/level-hints-template.jinja') as f:
        template = Template(f.read())

    for level_path in cfg.get_seeds():
        level_name = os.path.basename(level_path)
        if os.path.exists(f'core/framework/config/project.txt'):
            with open(f'core/levels/{level_path}/{level_name}.hints.html') as f:
                # Split hints in file
                blocks = f.read().split('\n---\n')
            # Set jinja args, indenting html tags that are mnot on the first line
            jinja_args = {'level_path': level_path,
                          'intro': blocks[0].replace('\n<', f'\n{" "*6}<'),
                          'hints': [block.replace('\n<', f'\n{" "*6}<') for block in blocks[1:-1]],
                          'writeup': blocks[-1].replace('\n<', f'\n{" "*4}<')}

            render = template.render(**jinja_args)
            if not os.path.exists(f'docs/{os.path.dirname(level_path)}'):
                os.makedirs(f'docs/{os.path.dirname(level_path)}')
            with open(f'docs/{level_path}.html', 'w+') as f:
                f.write(render)
        else:
            print(
                f'No hints file found for level: {level_path} at core/framework/config/project.txt')

Functions

def add_level(level_path)

Generates a seed for a new level, which is necessary to generate level secrets.

Parameters

level_path : str
The path of the level
Source code
def add_level(level_path):
    '''Generates a seed for a new level, which is necessary to generate level secrets.

    Parameters:
        level_path (str): The path of the level
    '''
    # Check to see if level already has a seed
    if level_path in cfg.get_seeds():
        exit(f'{level_path} has already been imported.')
    # Check to see if level has the necessary files
    level_name = os.path.basename(level_path)
    level_py_path = f'core/levels/{level_path}/{level_name}.py'
    level_yaml_path = f'core/levels/{level_path}/{level_name}.yaml'
    if not os.path.exists(level_py_path):
        exit(f'Expected level python file was not found at {level_py_path}')
    if not os.path.exists(level_yaml_path):
        exit(
            f'Expected yaml configuration file was not found at {level_yaml_path}')
    # Generate a random seed for the specified level
    seeds = cfg.get_seeds()
    seeds[level_path] = str(random.randint(100000, 999999))
    cfg.set_seeds(seeds)
def delete_start_files()

Deletes the start files of a level. This function should be called upon level destruction.

Source code
def delete_start_files():
    '''Deletes the start files of a level. This function should be called upon level destruction.'''
    shutil.rmtree('start')
def generate_level_docs()

Generates HTML documents for each level based on each level's [levelname].hints.html

Source code
def generate_level_docs():
    '''Generates HTML documents for each level based on each level's [levelname].hints.html'''
    with open('core/framework/level-hints-template.jinja') as f:
        template = Template(f.read())

    for level_path in cfg.get_seeds():
        level_name = os.path.basename(level_path)
        if os.path.exists(f'core/framework/config/project.txt'):
            with open(f'core/levels/{level_path}/{level_name}.hints.html') as f:
                # Split hints in file
                blocks = f.read().split('\n---\n')
            # Set jinja args, indenting html tags that are mnot on the first line
            jinja_args = {'level_path': level_path,
                          'intro': blocks[0].replace('\n<', f'\n{" "*6}<'),
                          'hints': [block.replace('\n<', f'\n{" "*6}<') for block in blocks[1:-1]],
                          'writeup': blocks[-1].replace('\n<', f'\n{" "*4}<')}

            render = template.render(**jinja_args)
            if not os.path.exists(f'docs/{os.path.dirname(level_path)}'):
                os.makedirs(f'docs/{os.path.dirname(level_path)}')
            with open(f'docs/{level_path}.html', 'w+') as f:
                f.write(render)
        else:
            print(
                f'No hints file found for level: {level_path} at core/framework/config/project.txt')
def import_level(level_path)

Returns the imported python module of the given level path.

Parameters

level_path : str
Relative path of level from core/levels/ directory

Returns

module
The python module of the given level
Source code
def import_level(level_path):
    '''Returns the imported python module of the given level path.

    Parameters:
        level_path (str): Relative path of level from core/levels/ directory
    
    Returns:
        module: The python module of the given level
    '''
    # Check if level is in config
    if not level_path in cfg.get_seeds():
        exit(
            f'Level: {level_path} not found in levels list. A list of available levels can be found by running:\n'
            '  python3 thunder.py list_levels\n'
            'If this is a custom level that you have not yet imported, run:\n'
            '  python3 thunder.py add_levels [level-path]')

    level_name = os.path.basename(level_path)
    try:
        level_module = importlib.import_module(
            f'.levels.{level_path.replace("/", ".")}.{level_name}', package='core')
    except ImportError:
        raise ImportError(
            f'Cannot import level: {level_path}. Check above error for details.')
    return level_module
def make_secret(level_path, chars=None)

Generates the secret of the level by hashing the level seed and the player's project id.

Parameters

level_path : str
The path of the level
chars : int, optional
Integer that sets the length of the returned secret. If not supplied, the secret will be arbitrary length based on the value of the hash.

Returns

str
String that contains an integer version of the hash of the level seed and project id
Source code
def make_secret(level_path, chars=None):
    '''Generates the secret of the level by hashing the level seed and the player's project id.

    Parameters:
        level_path (str): The path of the level
        chars (int, optional): Integer that sets the length of the returned secret. If not supplied, the secret will be arbitrary length based on the value of the hash.

    Returns:
        str: String that contains an integer version of the hash of the level seed and project id 
    '''
    credentials, project_id = google.auth.default()
    seeds = cfg.get_seeds()
    seed = seeds[level_path]
    if(not chars):
        return str(int(hashlib.sha1((seed+project_id).encode('utf-8')).hexdigest(), 16))
    else:
        return str(int(hashlib.sha1((seed+project_id).encode('utf-8')).hexdigest(), 16))[:chars]
def write_start_info(level_path, message, file_name=None, file_content=None)

Prints the start message and saves start files.

Prints the supplied start message and saves it to a text file, and saves another optional file, which can be used for credential files, ssh key files, or any other file that the player is given at the beginning of the level. This function saves files in the start/ directory.

Parameters

level_path : str
The path of the level being created
message : str
The start message that will be printed and saved
file_name : str, optional
The name of the optional extra file
file_content : str, optional
The contents of the optional extra file
Source code
def write_start_info(level_path, message, file_name=None, file_content=None):
    '''Prints the start message and saves start files.
    
    Prints the supplied start message and saves it to a text file, and saves another optional file, which can be used for credential files, ssh key files, or any other file that the player is given at the beginning of the level. This function saves files in the start/ directory.

    Parameters:
        level_path (str): The path of the level being created
        message (str): The start message that will be printed and saved
        file_name (str, optional): The name of the optional extra file
        file_content (str, optional): The contents of the optional extra file
    '''
    print('\n')
    # If start directory is not present, create it
    if not os.path.exists('start'):
        os.makedirs('start')
    # If there is an extra file, create it
    if file_name and file_content:
        file_path = f'start/{file_name}'
        with open(file_path, 'w+') as f:
            f.write(file_content)
        os.chmod(file_path, 0o400)
        print(
            f'Starting file: {file_name} has been written to {file_path}')
    # Write the start message to a file 
    level_name = os.path.basename(level_path)
    message_file_path = f'start/{level_name}.txt'
    with open(message_file_path, 'w+') as f:
        f.write(message)
    os.chmod(message_file_path, 0o400)
    print(
        f'Starting message for {level_path} has been written to {message_file_path}')
    # Print start message
    print(f'Start Message: {message}')
    print('\n')