Mentions légales du service

Skip to content
Snippets Groups Projects
Commit 5d443336 authored by PEDERSEN Ny Aina's avatar PEDERSEN Ny Aina
Browse files

Merge branch 'level_events'

parents 2b41d531 9ebedd19
No related branches found
No related tags found
No related merge requests found
Showing
with 313 additions and 61 deletions
Subproject commit 03b7826f06a4e6b7b423d6626766b099d952ad4f
Subproject commit 6f9b2a2cfb1911beebb2f9015fb41d86e50b0810
......@@ -2,6 +2,7 @@
from abc import ABC, abstractmethod
from collections import defaultdict
from typing import Callable, Optional, Any
import subprocess
import curses
import curses.textpad
......@@ -12,15 +13,80 @@ from easytracker import init_tracker, PauseReasonType
from level.arcade_ui import ArcadeUI
from level.utils import create_map
# pylint: disable=too-many-instance-attributes fixme
class EventList:
"""
Event list class.
Stores events in a dictionnary of events.
The value of this dictionnary are callback functions.
"""
def __init__(self, variables):
"""
Event list initialization.
:param variables: Dictionnary of variables.
"""
self.events = defaultdict(set)
self.variables = variables
def connect(self, event: str, function: Callable[[Any], None]) -> None:
"""
Connect a function to an event.
:param event: The name of the event to listen.
:param function: The function to call with the updated value.
"""
self.events[event].add(function)
def disconnect(
self,
function: Callable[[Any], None],
event: Optional[str] = None
) -> None:
"""
Disconnect a function from an event.
:param function: The function to disconnect.
:param event: The event to work on. If not given, disconnects the
function from all the events.
"""
if event is not None:
self.events[event].remove(function)
else:
for function_set in self.events.values():
function_set.discard(function)
def on_value_change(self, variable_list: list[str]) -> None:
"""
Trigger the update of list of variables.
:param variable_list: List of variables that have been updated.
"""
for variable in variable_list:
for function in self.events[f"value_change:{variable}"]:
function(self.variables[variable])
def trigger(self, event: str, *args: Optional[list[Any]]) -> None:
"""
Trigger an event.
:param event: The event to trigger.
:param args: The arguments to give to the functions connected to the event.
"""
for function in self.events[event]:
function(*args)
class AbstractLevel(ABC):
"""
Abstract level class that allows to use different
configurations.
"""
def __init__(self, metadata_dict):
def __init__(self, metadata_dict: dict):
"""
Initialize the level
......@@ -57,8 +123,10 @@ class AbstractLevel(ABC):
self.function_name = "main"
# Tracker
self.tracker = init_tracker("GDB")
# Event list
self.event_list = EventList(self.variables)
def run_tracker(self):
def run_tracker(self) -> None:
"""Start the tracker and loads the binary program."""
self.tracker.load_program(self.program_name)
self.tracker.start()
......@@ -71,7 +139,7 @@ class AbstractLevel(ABC):
for func in self.track:
self.tracker.track_function(func)
def handle_watch(self):
def handle_watch(self) -> list[str]:
"""
Updates the variables value change when a watchpoint is
triggered.
......@@ -82,7 +150,7 @@ class AbstractLevel(ABC):
:return: List of variable that have changed.
"""
def update_vars(args):
def update_vars(args: list[str]) -> Optional[str]:
"""
Update the variables by reading the arguments of a watchpoint.
......@@ -100,7 +168,7 @@ class AbstractLevel(ABC):
return var
updated_vars = []
pause_reason = self.tracker.get_pause_reason()
pause_reason = self.tracker.pause_reason
# TODO check if it is a user defined watch
# -> waiting for the easytracker feature
......@@ -108,7 +176,7 @@ class AbstractLevel(ABC):
# Going up in frames
if not user_defined:
pause_reason = self.tracker.get_pause_reason()
pause_reason = self.tracker.pause_reason
# Continue til the correct breakpoint
while True:
......@@ -131,7 +199,7 @@ class AbstractLevel(ABC):
return updated_vars # Variables that have changed
def handle_bp(self, finish: bool = True):
def handle_bp(self, finish: bool = True) -> list[str]:
"""
Test if a breakpoint is user defined or not.
If not, read the values of the variables of the tracked
......@@ -161,18 +229,18 @@ class AbstractLevel(ABC):
return updated_vars # Variables that have changed
def show_next_line(self):
def show_next_line(self) -> None:
"""Highlight the next line in the remote vim server."""
with subprocess.Popen(
"vim --servername tuto --remote-expr "
f"'HighlightLine({self.tracker.get_next_lineno()})'",
f"'HighlightLine({self.tracker.next_lineno})'",
shell=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, # silent if no server found
) as process:
process.wait()
def add_short_cmds(self):
def add_short_cmds(self) -> None:
"""Extend the available commands with the gdb aliases."""
short_cmd = []
for command in self.available_commands:
......@@ -193,7 +261,7 @@ class AbstractLevel(ABC):
short_cmd.append(cmd)
self.available_commands.extend(short_cmd)
def parse_and_eval(self, cmd: str):
def parse_and_eval(self, cmd: str) -> None:
"""
Parse a command and call the appropriate function.
......@@ -223,7 +291,7 @@ class AbstractLevel(ABC):
sys.exit()
# TODO add other commands (break, condition, ...)
def player_char(self, direction):
def player_char(self, direction: int) -> str:
"""Return the oriented character"""
up_value = int(self.alias["up"])
down_value = int(self.alias["down"])
......@@ -241,7 +309,7 @@ class AbstractLevel(ABC):
# Unknown direction
return "?"
def update_global_variables(self):
def update_global_variables(self) -> list[str]:
"""
Update the global variables.
......@@ -259,7 +327,7 @@ class AbstractLevel(ABC):
return updated_vars
def get_variable(self, var: str):
def get_variable(self, var: str) -> Any:
"""
Fetch the value of a variable.
......@@ -276,7 +344,7 @@ class AbstractLevel(ABC):
return ret
def eval_condition(self, condition: str):
def eval_condition(self, condition: str) -> Optional[bool]:
"""
Evaluates a condition while making sure that we use
the inferior variable names for the information like
......@@ -289,9 +357,12 @@ class AbstractLevel(ABC):
condition = condition.replace(var, inferior_var)
# pylint: disable=eval-used
return eval(condition, {}, self.variables)
try:
return eval(condition, {}, self.variables)
except AttributeError:
return None # Missing variable. Hence false condition
def available_wowm(self):
def available_wowm(self) -> list[tuple[int, int]]:
"""Return a list of WOWM coordinates."""
coordinates = []
for wowm in self.wowms.values():
......@@ -319,7 +390,7 @@ class AbstractLevel(ABC):
return coordinates
def get_wowm_message(self):
def get_wowm_message(self) -> dict[str, list[str]]:
"""
Return a dictionnary of messages, with the WOWM
name as key and its message as value (as a list of lines).
......@@ -359,7 +430,7 @@ class AbstractLevel(ABC):
return message_dict
def get_ascii_map(self):
def get_ascii_map(self) -> list[list[str]]:
"""Return a list that represents the map."""
def enum_to_char(enum: str) -> str:
......@@ -396,7 +467,7 @@ class AbstractLevel(ABC):
class LevelText(AbstractLevel):
"""Level using text output."""
def show_map(self):
def show_map(self) -> None:
"""Print a ascii map."""
# Fetching the map
ascii_map = self.get_ascii_map()
......@@ -422,13 +493,13 @@ class LevelText(AbstractLevel):
for line in ascii_map:
print("".join(line))
def print_wowm_msg(self):
def print_wowm_msg(self) -> None:
"""Print the WOWM message."""
for wowm_id, message_list in self.get_wowm_message().items():
wowm_message = "\n".join(message_list)
print(f"WOMW{wowm_id} says:\n{wowm_message}")
def read_cmd(self):
def read_cmd(self) -> str:
"""Read a command from keyboard."""
try:
cmd = input(self.prompt)
......@@ -436,11 +507,13 @@ class LevelText(AbstractLevel):
cmd = "q"
return cmd
def run(self):
def run(self) -> None:
"""Run in a text interface."""
self.run_tracker()
while self.tracker.get_pause_reason().type != PauseReasonType.EXITED:
while self.tracker.pause_reason.type != PauseReasonType.EXITED:
updated_vars = []
stdout = self.tracker.get_inferior_stdout()
if stdout:
print(f"stdout: {stdout}")
......@@ -449,19 +522,22 @@ class LevelText(AbstractLevel):
self.function_name = self.tracker.get_current_function_name()
self.show_next_line()
self.update_global_variables()
updated_vars += self.update_global_variables()
self.show_map()
self.print_wowm_msg()
cmd = self.read_cmd()
self.parse_and_eval(cmd)
pause_type = self.tracker.get_pause_reason().type
pause_type = self.tracker.pause_reason.type
if pause_type is PauseReasonType.WATCHPOINT:
self.handle_watch()
updated_vars += self.handle_watch()
elif pause_type is PauseReasonType.BREAKPOINT:
self.handle_bp()
updated_vars += self.handle_bp()
if self.tracker.get_exit_code() == 0:
# Trigger value updates
self.event_list.on_value_change(updated_vars)
if self.tracker.exit_code == 0:
print("VICTORY")
else:
print("DEFEAT")
......@@ -471,7 +547,11 @@ class LevelCurses(AbstractLevel):
"""Level using curses."""
# pylint: disable=too-many-locals
def show_map(self, stdscr, pad):
def show_map(
self,
stdscr: curses.window,
pad: curses.window
) -> None:
"""Update the cursed map according the variables."""
pad.clear()
......@@ -510,7 +590,11 @@ class LevelCurses(AbstractLevel):
pad.refresh(0, 0, sminrow, smincol, smaxrow, smaxcol)
@staticmethod
def box_msg(stdscr, message_list, title=""):
def box_msg(
stdscr: curses.window,
message_list: list[str],
title: str = ""
) -> None:
"""
Show a message in a box.
......@@ -544,7 +628,12 @@ class LevelCurses(AbstractLevel):
# Wait for confirmation
stdscr.getkey()
def read_cmd(self, stdscr, input_win, resize_func):
def read_cmd(
self,
stdscr: curses.window,
input_win: curses.window,
resize_func: Callable[[], None]
) -> str:
"""
Read a command while handling resizes.
......@@ -564,7 +653,7 @@ class LevelCurses(AbstractLevel):
# Show the cursor
curses.curs_set(True)
def on_resize():
def on_resize() -> None:
"""Update the windows on resize."""
# Update the map
resize_func()
......@@ -576,7 +665,7 @@ class LevelCurses(AbstractLevel):
stdscr.addstr(lines - 1, 0, self.prompt)
stdscr.refresh()
def validator(char):
def validator(char: int) -> int:
"""
Textpad validator.
By default, the `edit` method of the Textbox instance
......@@ -598,7 +687,7 @@ class LevelCurses(AbstractLevel):
return cmd
def print_wowm_msg(self, stdscr):
def print_wowm_msg(self, stdscr: curses.window) -> None:
"""
Print the WOWM message.
......@@ -607,10 +696,10 @@ class LevelCurses(AbstractLevel):
for wowm_id, wowm_message in self.get_wowm_message().items():
self.box_msg(stdscr, wowm_message, title=f"WOWM{wowm_id}")
def run(self):
def run(self) -> None:
"""Run in a curses interface."""
def runner(stdscr):
def runner(stdscr: curses.window):
"""Runner that will be called by the curses wrapper."""
self.run_tracker()
......@@ -622,9 +711,11 @@ class LevelCurses(AbstractLevel):
curses.curs_set(False)
stdscr.clear()
while self.tracker.get_pause_reason().type != PauseReasonType.EXITED:
while self.tracker.pause_reason.type != PauseReasonType.EXITED:
updated_vars = []
self.show_next_line()
self.update_global_variables()
updated_vars += self.update_global_variables()
self.show_map(stdscr, pad)
self.print_wowm_msg(stdscr)
......@@ -643,13 +734,16 @@ class LevelCurses(AbstractLevel):
if stdout:
self.box_msg(stdscr, stdout.split("\n"), title="stdout")
pause_type = self.tracker.get_pause_reason().type
pause_type = self.tracker.pause_reason.type
if pause_type is PauseReasonType.WATCHPOINT:
self.handle_watch()
updated_vars += self.handle_watch()
elif pause_type is PauseReasonType.BREAKPOINT:
self.handle_bp()
updated_vars += self.handle_bp()
# Trigger value updates
self.event_list.on_value_change(updated_vars)
if self.tracker.get_exit_code() == 0:
if self.tracker.exit_code == 0:
self.box_msg(stdscr, ["VICTORY"])
else:
self.box_msg(stdscr, ["DEFEAT"])
......@@ -660,7 +754,7 @@ class LevelCurses(AbstractLevel):
class LevelArcade(AbstractLevel):
"""Level using arcade."""
def __init__(self, metadata_dict):
def __init__(self, metadata_dict: dict):
# Parent initialization
super().__init__(metadata_dict)
......@@ -694,7 +788,7 @@ class LevelArcade(AbstractLevel):
self.gui.start()
self.run_tracker()
while self.tracker.get_pause_reason().type != PauseReasonType.EXITED:
while self.tracker.pause_reason.type != PauseReasonType.EXITED:
self.show_next_line()
self.update_global_variables()
self.update_map()
......@@ -703,14 +797,14 @@ class LevelArcade(AbstractLevel):
self.parse_and_eval(cmd)
# TODO write this in the GUI
if self.tracker.get_exit_code() == 0:
if self.tracker.exit_code == 0:
print("VICTORY")
else:
print("DEFEAT")
# pylint: disable=redefined-builtin
def level(metadata, type="arcade"):
def level(metadata, type: str = "arcade"):
"""
Return the level for a given metadata.
......
File deleted
**/*.o
**/test
#!/usr/bin/env bash
# run all the tests
path="$(realpath "$0")"
dir="$(dirname "$path")"
for test_dir in "$dir"/*; do
# run `make run` inside the directory
if [[ -d $test_dir ]]; then
make run -C "$test_dir"
# Stop if the test failed
if [[ $? != 0 ]]; then
echo Test "$(basename "$test_dir")" failed.
exit $?
fi
fi
done
echo Tests ok.
# vim:set ts=8 sts=2 sw=2 et:
CC := clang
CFLAGS := -Wall -g -Werror -pedantic
EXE := test
SILENT := &>/dev/null
test: test.c
.PHONY: clean clear run
run: test
./main.py ${SILENT} < input.txt
clean:
rm -f *.o
clear: clean
rm -f ${EXE}
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
next
#!/usr/bin/env python3
"""Value change event test."""
from level.utils import generate_dict
from level.level import level
# List that stores and the values of the variable that we watch
VALUES = []
def event_callback(new_value: int) -> None:
"""
Append the new value to the global list
when a value_change event is received.
"""
VALUES.append(new_value)
def main() -> None:
"""
Creates a level, and connects the previous function
to the value_change:player_x event.
"""
# Level creation
metadata = generate_dict("test.c")
game = level(metadata, type="text")
# Creating custom events
game.event_list.connect("value_change:player_x", event_callback)
game.run()
def check() -> None:
"""Check the result."""
expected = list(range(10))
# Checking the list
print(f"Received: {VALUES}, expected {expected}")
assert VALUES == list(range(10))
if __name__ == "__main__":
main()
check()
#include <stdio.h>
/*
* Creates a global variable.
* And increments it 10 times.
*/
/* @AGDB
* program_name: test
* map_width: 20
* map_height: 10
* available_commands: next
* alias: player_y 5
* alias: player_direction 3
* alias: right 3
* alias: exit_y 5
* alias: exit_x 9
*/
int player_x = 0;
int main() {
for (int i = 0; i < 10; ++i)
player_x = i;
return 0;
}
File moved
......@@ -58,7 +58,7 @@ struct {
int exit_x = 10,
exit_y = 3;
// @AGDB watch: exit_x hum
// @AGDB watch: exit_x
int main(void) {
player.x = 3;
......
......@@ -30,18 +30,9 @@
"#offbyone",
"#fixedexit"
],
"track": {
"main": [
"ma_var"
],
"my_func": [
"my_other_var",
"my_other_var2"
]
},
"track": {},
"watch": [
"exit_x",
"hum"
"exit_x"
],
"wowms": {
"1": {
......
File moved
File moved
File moved
File moved
File moved
File moved
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment