1
0
Fork 0
This repository has been archived on 2024-01-30. You can view files and clone it, but cannot push or open issues or pull requests.
adaptive-game-assistant/adaptive_game_module/adaptive_game.py

241 lines
9.4 KiB
Python

import subprocess
import os
import time
import yaml # reads level configurations from a .yml file
class NoLevelFoundError(Exception):
"""Error raised by the Game class when a nonexistant level is selected."""
def __init__(self, message):
self.message = message
class Game:
"""Class representing the adaptive game, its progress, setup+solving times.
An object of the class will remember the possible branching of the game,
it will keep track of the level and branch the player is on,
and it will save loading times and solving times of the player."""
def __init__(self, mapping_file, game_directory,
level_number=0, level_branch=''):
"""Create an object of class Game.
The game itself is NOT started at this point. To start it, call
self.start_game() next.
Parameters
----------
mapping_file
location of the .yml file with level mappings.
game_directory
directory with the game files (Vagrantfile, etc.)
levelNumber
number of level to start on
(useful when continuing a running game)
levelBranch
branch of level being started on
(useful when continuing a running game)"""
self.game_directory = game_directory
self.read_mapping(mapping_file)
self.load_times = []
self.solving_times = []
self.level_log = []
self.game_start_time = 0
self.level_start_time = 0
self.level = level_number
self.branch = level_branch
self.game_in_progress = False
self.game_finished = False
self.game_end_time = 0
def start_game(self):
"""Start a new game, if there isn't one already in progress."""
if (self.game_in_progress or self.game_finished):
return False
self.game_in_progress = True
self.level = 1
self.level_log.append("level1") # add level 1 to log
start_time = time.time()
subprocess.run(["vagrant", "up"], cwd=self.game_directory,
env=dict(os.environ, ANSIBLE_ARGS='--tags \"setup\"'))
end_time = time.time()
load_time = int(end_time - start_time)
self.load_times.append(load_time)
self.level_start_time = time.time()
self.game_start_time = time.time()
return True
def finish_game(self):
"""Mark game as not in progress and log end time, if on the last level.
Return false if prerequisites were not met, true otherwise."""
if (self.next_level_exists() or not self.game_in_progress):
return False
self.solving_times.append(int(time.time() - self.level_start_time))
self.game_end_time = time.time()
self.game_in_progress = False
self.game_finished = True
return True
def next_level_exists(self):
"""Return true if next level exists."""
next_level = "level" + str(self.level + 1)
return next_level in self.level_mapping
def next_level_is_forked(self):
"""Return true if next level is forked."""
next_level = "level" + str(self.level + 1)
if next_level in self.level_mapping:
if len(self.level_mapping[next_level]) > 1:
return True
return False
def next_level_branch_check(self, input_text):
"""Check if `input_text` is a branch of next level, return next level branch name.
`input_text` can be a full level name, partial level name or just
a branch letter. Ex. "level4a", "4a" and "a" are all fine."""
next_level = "level" + str(self.level + 1)
if "level" + str(self.level + 1) + input_text in self.level_mapping[next_level]:
return input_text
if "level" + input_text in self.level_mapping[next_level]:
return input_text[1:]
if input_text in self.level_mapping[next_level]:
return input_text[6:]
raise NoLevelFoundError("No branch called {} found.".format(input_text))
def next_level(self, next_branch_input=""):
"""Advance the game to next level with branch `next_branch_input`.
Because `next_branch_input` can be supplied by user, perform check
if it is real first.
Raise NoLevelFoundError if there is no such level present.
next_branch_input == '' is understood as no branch selected, a level
without possible branching."""
next_branch = ""
if not self.game_in_progress:
raise NoLevelFoundError("Can't continue, (S)tart the game first!")
if not self.next_level_exists():
raise NoLevelFoundError("No next level found! Perhaps you already finished the game?")
if self.next_level_is_forked():
next_branch = self.next_level_branch_check(next_branch_input)
# raises NoLevelFoundError
elif next_branch_input != "":
raise NoLevelFoundError("Next level has no branch, but branch was given.")
self.solving_times.append(int(time.time() - self.level_start_time))
self.level += 1
self.branch = next_branch
level_name = "level" + str(self.level) + self.branch
self.level_log.append(level_name)
boxes = self.level_mapping["level" + str(self.level)][level_name]
start_time = time.time()
subprocess.run(["vagrant", "up"] + boxes + ["--provision"], cwd=self.game_directory,
env=dict(os.environ, ANSIBLE_ARGS='--tags \"' + level_name + '\"'))
end_time = time.time()
load_time = int(end_time - start_time)
self.load_times.append(load_time)
self.level_start_time = time.time()
def abort_game(self):
"""Abort game and reset attributes to their default state."""
subprocess.run(["vagrant", "destroy", "-f"], cwd=self.game_directory)
self.load_times = []
self.solving_times = []
self.level_log = []
self.game_start_time = 0
self.level_start_time = 0
self.game_in_progress = False
self.game_finished = False
self.level = 0
self.branch = ''
def running_time(self):
"""Return running time in minutes."""
current_time = time.time()
total_time_seconds = int(current_time - self.game_start_time)
return int(total_time_seconds/60)
# # # METHODS THAT WORK WITH FILES # # #
def read_mapping(self, filename): # raise OSError when file can't be opened
"""Read a mapping of levels for the adaptive game from a YAML file."""
with open(filename) as f:
self.level_mapping = yaml.load(f, Loader=yaml.FullLoader)
def log_to_file(self, filename): # raise OSError when file can't be opened
"""Log the current game state and logs into a YAML file."""
with open(filename, 'a') as f:
yaml.dump(self, f)
# # # METHODS THAT OUTPUT INTO STDOUT # # #
def print_next_level_fork_names(self):
"""Print names of next level forks."""
next_level = "level" + str(self.level + 1)
print("Next level branches are:")
if next_level in self.level_mapping:
for branch in self.level_mapping[next_level]:
print(branch, end=" ")
print("")
def print_time(self, load_time, concise=False):
"""Print time in minutes and seconds.
If concise is True, prints only numbers and letters."""
if load_time < 60:
if concise:
print("{}s".format(load_time))
else:
print("Time elapsed: {} seconds.".format(load_time))
else:
minutes = load_time // 60
seconds = load_time % 60
if concise:
print("{}m{}s".format(minutes, seconds))
else:
print("Time elapsed: {} minutes, {} seconds.".format(minutes, seconds))
def print_info(self):
"""Print info about the game in a human-readable way."""
if not self.game_in_progress and not self.game_finished:
print("Game is not yet started.")
else:
if self.game_in_progress: # in progress implies not finished
if self.branch:
print("Game in progress. Level:{} Branch:{}".format(self.level, self.branch))
else:
print("Game in progress. Level:{}".format(self.level))
print("Total time elapsed: ", end="")
self.print_time(int(time.time() - self.game_start_time), True)
else:
print("Game succesfully finished.")
print("Total time played: ", end="")
self.print_time(int(self.game_end_time - self.game_start_time), True)
if self.level_log:
print("Levels traversed:")
for level in self.level_log:
print("{} ".format(level), end="")
print("")
print("Loading times:")
for i in range(len(self.load_times)):
if i == 0:
print("Setup + ", end="")
print("{}: ".format(self.level_log[i]), end="")
self.print_time(self.load_times[i], True)
if self.solving_times:
print("Solving times:")
for i in range(len(self.solving_times)):
print("{}: ".format(self.level_log[i]), end="")
self.print_time(self.solving_times[i], True)