Skip to content

Commit 564bde8

Browse files
committed
Added proper Death parsing
- Multiple killers are supported - Assists are supported - Summoned creatures supported
1 parent c9f8e87 commit 564bde8

File tree

6 files changed

+148
-47
lines changed

6 files changed

+148
-47
lines changed

.coveragerc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,8 @@ include =
66
exclude_lines =
77
pragma: no cover
88
if __name__ == .__main__.:
9-
def __repr__
9+
def __repr__
10+
def __eq__
11+
def __len__
12+
def __lt__
13+
def __gt__

docs/index.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ Guild Member
9494
:members:
9595
:inherited-members:
9696

97+
Killer
98+
----------------
99+
.. autoclass:: Killer
100+
:members:
101+
97102
Other Character
98103
-----------------
99104
.. autoclass:: OtherCharacter

tests/resources/character_deaths_complex.txt

Lines changed: 11 additions & 2 deletions
Large diffs are not rendered by default.

tests/tests_character.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import datetime
22

33
from tests.tests_tibiapy import TestTibiaPy
4-
from tibiapy import Character
4+
from tibiapy import Character, Death, Killer
55
from tibiapy.utils import parse_tibia_datetime
66

77
FILE_CHARACTER_RESOURCE = "character_regular.txt"
@@ -58,8 +58,16 @@ def testCharacterDeletion(self):
5858

5959
def testCharacterComplexDeaths(self):
6060
content = self._load_resource(FILE_CHARACTER_DEATHS_COMPLEX)
61-
parsed_content = Character._beautiful_soup(content)
62-
tables = Character._parse_tables(parsed_content)
63-
self.assertTrue("Character Deaths" in tables.keys())
64-
char = {}
65-
Character._parse_deaths(char, tables["Character Deaths"])
61+
char = Character.from_content(content)
62+
self.assertTrue(char.deaths)
63+
self.assertEqual(len(char.deaths), 19)
64+
65+
def testDeathTypes(self):
66+
assisted_suicide = Death("Galarzaa", 280, killers=[Killer("Galarzaa", True), Killer("a pixy")],
67+
time=datetime.datetime.now())
68+
self.assertEqual(assisted_suicide.killer, assisted_suicide.killers[0])
69+
self.assertFalse(assisted_suicide.by_player)
70+
71+
spawn_invasion = Death("Galarza", 270, killers=[Killer("a demon"), Killer("Nezune", True)])
72+
self.assertEqual(spawn_invasion.killer, spawn_invasion.killers[0])
73+
self.assertTrue(spawn_invasion.by_player)

tibiapy/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from . import abc, utils
2-
from .character import Character, Death, OtherCharacter
2+
from .character import Character, Death, OtherCharacter, Killer
33
from .const import *
44
from .errors import *
55
from .guild import Guild, GuildMember, GuildInvite
66

7-
__version__ = '0.1.0a5'
7+
__version__ = '0.1.0'

tibiapy/character.py

Lines changed: 111 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,15 @@
1212
from .utils import parse_tibia_datetime
1313

1414
deleted_regexp = re.compile(r'([^,]+), will be deleted at (.*)')
15-
death_regexp = re.compile(r'Level (\d+) by ([^.]+)')
15+
# Extracts the death's level and killers.
16+
death_regexp = re.compile(r'Level (?P<level>\d+) by (?P<killers>.*)\.</td>')
17+
# From the killers list, filters out the assists.
18+
death_assisted = re.compile(r'(?P<killers>.+)\.<br/>Assisted by (?P<assists>.+)')
19+
# From a killer entry, extracts the summoned creature
20+
death_summon = re.compile(r'(?P<summon>.+) of <a[^>]+>(?P<name>[^<]+)</a>')
21+
# Extracts the contents of a tag
22+
link_content = re.compile(r'>([^<]+)<')
23+
1624
house_regexp = re.compile(r'paid until (.*)')
1725
guild_regexp = re.compile(r'([\s\w]+)\sof the\s(.+)')
1826

@@ -271,26 +279,41 @@ def _parse_deaths(char, rows):
271279
"""
272280
deaths = []
273281
for row in rows:
274-
cols_raw = row.find_all('td')
275-
cols = [ele.text.strip() for ele in cols_raw]
276-
if len(cols) != 2:
277-
continue
278-
death_time, death = cols
282+
cols = row.find_all('td')
283+
death_time = cols[0].text.strip()
284+
death = str(cols[1]).replace("\xa0", " ")
279285
death_time = death_time.replace("\xa0", " ")
280286
death_info = death_regexp.search(death)
281287
if death_info:
282-
level = death_info.group(1)
283-
killer = death_info.group(2)
288+
level = int(death_info.group("level"))
289+
killers_str = death_info.group("killers")
284290
else:
285291
continue
286-
death_link = cols_raw[1].find('a')
287-
death_player = False
288-
if death_link:
289-
death_player = True
290-
killer = death_link.text.strip().replace("\xa0", " ")
292+
assists = []
293+
# Check if the killers list contains assists
294+
assist_match = death_assisted.search(killers_str)
295+
if assist_match:
296+
# Filter out assists
297+
killers_str = assist_match.group("killers")
298+
# Split assists into a list.
299+
assists = Character._split_list(assist_match.group("assists"))
300+
killers = Character._split_list(killers_str)
301+
for (i, killer) in enumerate(killers):
302+
# If the killer contains a link, it is a player.
303+
if "href" in killer:
304+
killer_dict = {"name": link_content.search(killer).group(1), "player": True}
305+
else:
306+
killer_dict = {"name": killer, "player": False}
307+
# Check if it contains a summon.
308+
m = death_summon.search(killer)
309+
if m:
310+
killer_dict["summon"] = m.group("summon")
311+
killers[i] = killer_dict
312+
for (i, assist) in enumerate(assists):
313+
# Extract names from character links in assists list.
314+
assists[i] = {"name": link_content.search(assist).group(1), "player": True}
291315
try:
292-
deaths.append({'time': death_time, 'level': int(level), 'killer': killer,
293-
'is_player': death_player})
316+
deaths.append({'time': death_time, 'level': level, 'killers': killers, 'assists': assists})
294317
except ValueError:
295318
# Some pvp deaths have no level, so they are raising a ValueError, they will be ignored for now.
296319
continue
@@ -353,11 +376,11 @@ def _split_list(items, separator=",", last_separator=" and "):
353376
separator: :class:`str`
354377
The separator between each item. A comma by default.
355378
last_separator: :class:`str`
356-
The separator used for the last imte. ' and ' by default.
379+
The separator used for the last item. ' and ' by default.
357380
358381
Returns
359382
-------
360-
List[str]
383+
List[:class:`str`]
361384
A list containing each one of the items.
362385
"""
363386
if items is None:
@@ -376,9 +399,9 @@ def parse_to_json(content, indent=None):
376399
377400
Parameters
378401
-------------
379-
content: str
402+
content: :class:`str`
380403
The HTML content of the page.
381-
indent: int
404+
indent: :class:`int`
382405
The number of spaces to indent the output with.
383406
384407
Returns
@@ -463,39 +486,40 @@ class Death:
463486
The name of the character this death belongs to.
464487
465488
level: :class:`int`
466-
The level at which the level occurred.
489+
The level at which the death occurred.
467490
468-
killer: :class:`str`
469-
The main killer.
491+
killers: List[:class:`Killer`]
492+
A list of all the killers involved.
493+
494+
assists: List[:class:`Killer`]
495+
A list of characters that were involved, without dealing damage.
470496
471497
time: :class:`datetime.datetime`
472498
The time at which the death occurred.
473-
474-
is_player: :class:`bool`
475-
True if the killer is a player, False otherwise.
476-
477-
participants: :class:`list`
478-
List of all participants in the death.
479499
"""
480-
__slots__ = ("level", "killer", "time", "is_player", "name", "participants")
500+
__slots__ = ("level", "killers", "time", "assists", "name")
481501

482-
def __init__(self, level=0, killer=None, time=None, is_player=False, name=None, participants=None):
502+
def __init__(self, name=None, level=0, **kwargs):
483503
self.name = name
484504
self.level = level
485-
self.killer = killer
505+
self.killers = kwargs.get("killers", [])
506+
if self.killers and isinstance(self.killers[0], dict):
507+
self.killers = [Killer(**k) for k in self.killers]
508+
self.assists = kwargs.get("assists", [])
509+
if self.assists and isinstance(self.assists[0], dict):
510+
self.assists = [Killer(**k) for k in self.assists]
511+
time = kwargs.get("time")
486512
if isinstance(time, datetime.datetime):
487513
self.time = time
488514
elif isinstance(time, str):
489515
self.time = parse_tibia_datetime(time)
490516
else:
491517
self.time = None
492-
self.is_player = is_player
493-
self.participants = participants or []
494518

495-
def __repr__(self) -> str:
519+
def __repr__(self):
496520
attributes = ""
497521
for attr in self.__slots__:
498-
if attr in ["level", "killer"]:
522+
if attr in ["name", "level"]:
499523
continue
500524
v = getattr(self, attr)
501525
if isinstance(v, int) and v == 0 and not isinstance(v, bool):
@@ -505,8 +529,59 @@ def __repr__(self) -> str:
505529
if v is None:
506530
continue
507531
attributes += ",%s=%r" % (attr, v)
508-
return "{0.__class__.__name__}({0.level!r},{0.killer!r}{1})".format(self, attributes)
532+
return "{0.__class__.__name__}({0.name!r},{0.level!r}{1})".format(self, attributes)
533+
534+
@property
535+
def killer(self):
536+
"""Optional[:class:`Killer`]: The first killer in the list.
537+
538+
This is usually the killer that gave the killing blow."""
539+
return self.killers[0] if self.killers else None
540+
541+
@property
542+
def by_player(self):
543+
""":class:`bool`: Whether the kill involves other characters."""
544+
return any([k.player and self.name != k.name for k in self.killers])
545+
546+
class Killer:
547+
"""
548+
Represents a killer.
549+
550+
A killer can be:
509551
552+
a) Another character.
553+
b) A creature.
554+
c) A creature summoned by a character.
555+
556+
Attributes
557+
-----------
558+
name: :class:`str`
559+
The name of the killer.
560+
player: :class:`bool`
561+
Whether the killer is a player or not.
562+
summon: Optional[:class:`str`]
563+
The name of the summoned creature, if applicable.
564+
"""
565+
__slots__ = ("name", "player", "summon")
566+
def __init__(self, name=None, player=False, summon=None):
567+
self.name = name
568+
self.player = player
569+
self.summon = summon
570+
571+
def __repr__(self):
572+
attributes = ""
573+
for attr in self.__slots__:
574+
if attr in ["name"]:
575+
continue
576+
v = getattr(self, attr)
577+
if isinstance(v, int) and v == 0 and not isinstance(v, bool):
578+
continue
579+
if isinstance(v, list) and len(v) == 0:
580+
continue
581+
if v is None:
582+
continue
583+
attributes += ",%s=%r" % (attr, v)
584+
return "{0.__class__.__name__}({0.name!r}{1})".format(self, attributes)
510585

511586
class OtherCharacter(abc.Character):
512587
"""

0 commit comments

Comments
 (0)