From 264109e3d44f1a888e469d19c8b40bb0e0c83489 Mon Sep 17 00:00:00 2001 From: silvatek <85451322+silvatek@users.noreply.github.com> Date: Sun, 8 Sep 2024 23:03:58 +0100 Subject: [PATCH] More combat log analysis, plus tests (#712) --- backend/combatlog/combatlog.py | 113 ++++++++++++++++++-------- backend/combatlog/router.py | 22 ++++- backend/combatlog/tests.py | 142 +++++++++++++++++++++++++++++++++ 3 files changed, 242 insertions(+), 35 deletions(-) create mode 100644 backend/combatlog/tests.py diff --git a/backend/combatlog/combatlog.py b/backend/combatlog/combatlog.py index d0d843e8..fc4657d4 100644 --- a/backend/combatlog/combatlog.py +++ b/backend/combatlog/combatlog.py @@ -1,3 +1,6 @@ +from typing import List, Dict + + class LogEvent: raw_log: str event_time: str @@ -6,9 +9,12 @@ class LogEvent: class DamageEvent: - damage: int + damage: int = 0 direction: str entity: str + weapon: str + outcome: str + text: str class LogAnalysis: @@ -17,37 +23,39 @@ class LogAnalysis: damage_taken: int -def parse(text): - events = [] - for line in text.splitlines(): - line = line.strip() +def parse_line(line: str) -> LogEvent: + line = line.strip() - if line.startswith("["): - pos = line.find("]") + if line.startswith("["): + pos = line.find("]") - event = LogEvent() + event = LogEvent() - event.event_time = line[1 : pos - 1].strip() - text = line[pos + 1 :].strip() + event.event_time = line[1 : pos - 1].strip() + text = line[pos + 1 :].strip() - pos = text.find(")") - if pos == -1: - event.event_type = "unknown" - else: - event.event_type = text[1:pos].strip() - text = text[pos + 1 :] + pos = text.find(")") + if pos == -1: + event.event_type = "unknown" + else: + event.event_type = text[1:pos].strip() + text = text[pos + 1 :] - event.text = strip_html(text) + event.text = strip_html(text) + else: + event = LogEvent() + event.event_time = "" + event.event_type = "unknown" + event.text = line - events.append(event) + return event - # print(event.event_time, "|", event.event_type, "|", event.text) - else: - event = LogEvent() - event.event_time = "" - event.event_type = "unknown" - event.text = line - events.append(event) + +def parse(text: str) -> List[LogEvent]: + events = [] + for line in text.splitlines(): + event = parse_line(line) + events.append(event) return events @@ -66,27 +74,42 @@ def strip_html(text): return text -def damage_events(events): +def damage_events(events: List[LogEvent]) -> List[DamageEvent]: dmg_events = [] for event in events: if event.event_type == "combat": damage_event = DamageEvent() - pos = event.text.find(" to ") + + text = event.text + + pos = text.find(" to ") if pos >= 0: - damage_event.damage = int(event.text[0:pos]) + damage_event.damage = int(text[0:pos]) damage_event.direction = "to" - dmg_events.append(damage_event) + text = text[pos + 4 :] - pos = event.text.find(" from ") + pos = text.find(" from ") if pos >= 0: - damage_event.damage = int(event.text[0:pos]) + damage_event.damage = int(text[0:pos]) damage_event.direction = "from" + text = text[pos + 6 :] + + parts = text.split("-") + if len(parts) >= 1: + damage_event.entity = parts[0].strip() + if len(parts) >= 2: + damage_event.weapon = parts[1].strip() + if len(parts) >= 3: + damage_event.outcome = parts[2].strip() + + if damage_event.damage > 0: + damage_event.text = text dmg_events.append(damage_event) return dmg_events -def damage_done(dmg_events): +def total_damage(dmg_events): total_done = 0 total_taken = 0 for event in dmg_events: @@ -97,3 +120,29 @@ def damage_done(dmg_events): total_taken += event.damage return (total_done, total_taken) + + +def enemy_damage( + dmg_events: List[DamageEvent], direction: str +) -> Dict[str, int]: + result = {} + + for event in dmg_events: + if event.direction == direction: + if event.entity not in result: + result[event.entity] = 0 + result[event.entity] += event.damage + + return result + + +def weapon_damage(dmg_events: List[DamageEvent]) -> Dict[str, int]: + result = {} + + for event in dmg_events: + if event.direction == "to": + if event.weapon not in result: + result[event.weapon] = 0 + result[event.weapon] += event.damage + + return result diff --git a/backend/combatlog/router.py b/backend/combatlog/router.py index c30cf76f..0a8ffab8 100644 --- a/backend/combatlog/router.py +++ b/backend/combatlog/router.py @@ -1,7 +1,14 @@ from ninja import Router from pydantic import BaseModel - -from .combatlog import parse, damage_events, damage_done +from typing import Dict + +from .combatlog import ( + parse, + damage_events, + total_damage, + enemy_damage, + weapon_damage, +) router = Router(tags=["combatlog"]) @@ -10,6 +17,9 @@ class LogAnalysis(BaseModel): logged_events: int = 0 damage_done: int = 0 damage_taken: int = 0 + damage_from_enemies: Dict[str, int] = {} + damage_to_enemies: Dict[str, int] = {} + damage_with_weapons: Dict[str, int] = {} @router.post( @@ -29,7 +39,13 @@ def analyze_logs(request): analysis = LogAnalysis() analysis.logged_events = len(events) + dmg_events = damage_events(events) - (analysis.damage_done, analysis.damage_taken) = damage_done(dmg_events) + + (analysis.damage_done, analysis.damage_taken) = total_damage(dmg_events) + + analysis.damage_from_enemies = enemy_damage(dmg_events, "from") + analysis.damage_to_enemies = enemy_damage(dmg_events, "to") + analysis.damage_with_weapons = weapon_damage(dmg_events) return analysis diff --git a/backend/combatlog/tests.py b/backend/combatlog/tests.py new file mode 100644 index 00000000..6e9cecd2 --- /dev/null +++ b/backend/combatlog/tests.py @@ -0,0 +1,142 @@ +from django.test import TestCase + +from .combatlog import ( + LogEvent, + DamageEvent, + parse, + parse_line, + strip_html, + damage_events, + total_damage, + enemy_damage, +) + + +class ParseCombatLogTest(TestCase): + def test_parse_line_combat(self): + log_line = "[ 2024.09.07 14:58:50 ] (combat) 567 to Angel Cartel Codebug - Inferno Rage Compiler Error - Hits" + event = parse_line(log_line) + self.assertEqual(event.event_time, "2024.09.07 14:58:50") + self.assertEqual(event.event_type, "combat") + self.assertEqual( + event.text, + "567 to Angel Cartel Codebug - Inferno Rage Compiler Error - Hits", + ) + + def test_parse_line_hint(self): + log_line = ( + "[ 2024.09.07 13:59:17 ] (hint) Attempting to join a channel" + ) + event = parse_line(log_line) + self.assertEqual(event.event_time, "2024.09.07 13:59:17") + self.assertEqual(event.event_type, "hint") + self.assertEqual(event.text, "Attempting to join a channel") + + def test_parse_line_session(self): + log_line = " Session Started: 2024.09.07 13:59:16" + event = parse_line(log_line) + self.assertEqual(event.event_time, "") + self.assertEqual(event.event_type, "unknown") + self.assertEqual(event.text, "Session Started: 2024.09.07 13:59:16") + + def test_parse_logs(self): + logs = "ABC\nXYZ" + events = parse(logs) + self.assertEqual(2, len(events)) + + def test_parse_empty_logs(self): + logs = "" + events = parse(logs) + self.assertEqual(0, len(events)) + + +class StripHtmlTest(TestCase): + def test_strip_html_basic(self): + stripped = strip_html("hello world") + self.assertEqual("hello world", stripped) + + def test_strip_html_no_tags(self): + stripped = strip_html("hello world") + self.assertEqual("hello world", stripped) + + def test_strip_html_empty(self): + stripped = strip_html("") + self.assertEqual("", stripped) + + +def log_event(event_time, event_type, event_text): + event = LogEvent() + event.event_time = event_time + event.event_type = event_type + event.text = event_text + return event + + +def damage_event( + damage: int, direction: str, entity: str, weapon: str, outcome: str +) -> DamageEvent: + event = DamageEvent() + event.damage = damage + event.direction = direction + event.entity = entity + event.weapon = weapon + event.outcome = outcome + return event + + +class DamageParseTest(TestCase): + def test_find_damage_events(self): + events = [] + events.append(log_event("", "combat", "123 from Rat")) + events.append(log_event("", "combat", "345 to Rat")) + events.append(log_event("", "peace", "collaborating")) + + dmg_events = damage_events(events) + + self.assertEqual(2, len(dmg_events)) + + def test_total_damage_done(self): + events = [] + events.append(log_event("", "combat", "120 to Rat")) + events.append(log_event("", "combat", "140 to Rat")) + events.append(log_event("", "combat", "125 from Rat")) + events.append(log_event("", "combat", "155 from Rat")) + + dmg_events = damage_events(events) + + self.assertEqual(4, len(dmg_events)) + + (dmg_done, dmg_taken) = total_damage(dmg_events) + + self.assertEqual(260, dmg_done) + self.assertEqual(280, dmg_taken) + + def test_damage_parse(self): + events = [] + events.append( + log_event("", "combat", "120 from Rat - Sharp Teeth - Hits") + ) + + dmg_events = damage_events(events) + + self.assertEqual(120, dmg_events[0].damage) + self.assertEqual("from", dmg_events[0].direction) + self.assertEqual("Rat", dmg_events[0].entity) + self.assertEqual("Sharp Teeth", dmg_events[0].weapon) + self.assertEqual("Hits", dmg_events[0].outcome) + + def test_enemy_damage(self): + events = [] + events.append(damage_event(100, "from", "Rat", "Sharp Teeth", "Hits")) + events.append(damage_event(123, "from", "Bat", "Sharp Teeth", "Hits")) + events.append(damage_event(110, "from", "Rat", "Sharp Teeth", "Hits")) + events.append(damage_event(125, "to", "Rat", "Sharp Teeth", "Hits")) + + enemies = enemy_damage(events, "from") + + self.assertEqual(210, enemies["Rat"]) + self.assertEqual(123, enemies["Bat"]) + + enemies = enemy_damage(events, "to") + + self.assertEqual(125, enemies["Rat"])