Skip to content
This repository has been archived by the owner on Feb 17, 2023. It is now read-only.

LNX-69 Add bear #14

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
pytest==7.2.0
transitions==0.9.0
pytest==7.2.0
32 changes: 32 additions & 0 deletions src/actions/attacks/slam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import List

from src.common.point import Point
from src.actions.action import Action
from src.actions.deal_dmg import DealDmg
from src.objects.object import Object
from src.objects.npc import NPC
from src.objects.agent import Agent

SLAM_DMG = 10


class Slam(Action):
"""
Simple action for attacking.
"""
object: Object

def __init__(self, object: Object, points: List[Point]) -> None:
super().__init__()
self.properties.object_id = object.properties.id
self.object = object
self.properties.points = points
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This thing is a little tricky. Right now front-end does not support deserialization of lists but I think it should not be a problem to add something like that. I guess it will be enough to have some helper function in Deserializer to call strtok.


def execute(self) -> None:
self.log()
for target_position in self.properties.points:
if not self.object.scene._objects_position_map.get(target_position):
continue
for target in self.object.scene.get_objects_by_position(target_position):
if isinstance(target, NPC) or isinstance(target, Agent):
DealDmg(target, SLAM_DMG).execute()
2 changes: 1 addition & 1 deletion src/actions/destroy.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ def __init__(self, object: Union['Agent', NPC]) -> None:
self.object = object

def execute(self) -> None:
self.object.free_field(self.object.properties.position)
self.object.scene.remove_object_from_occupied_fields(self.object)
self.object.scene.remove_object_from_id_map(self.object)
self.log()
18 changes: 18 additions & 0 deletions src/actions/prepare_to_slam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from src.actions.action import Action
from src.common.serializable import Properties
from src.objects.object import Object


class PrepareToSlam(Action):
"""
Simple action for idling.
"""
object: Object

def __init__(self, object: Object) -> None:
super().__init__()
self.properties.object_id = object.properties.id
self.object = object

def execute(self) -> None:
self.log()
67 changes: 67 additions & 0 deletions src/common/state_machine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from typing import Union
from enum import Enum
from typing import Callable
from abc import ABC, abstractmethod


class Transition:
def __init__(self, from_state: Union[str, Enum], to_state: Union[str, Enum], condition_func: Callable[[], bool]):
self.from_state = from_state
self.to_state = to_state
self.condition_func = condition_func


class StateMachine(ABC):
"""
Simple state machine.
It checks transitions, and moves one state at a time (if possible) during tick() method.
States can be virtually anything, but best kept as either strings or enums.
"""

def __init__(self):
self._state = None
self._previous_state = None
self._states = {}
self._transitions = {}

@abstractmethod
def _state_logic(self):
pass

def _get_transition(self):
for transition in self._transitions[self._state]:
if transition.condition_func():
return transition.to_state
return None
alpinus4 marked this conversation as resolved.
Show resolved Hide resolved

@abstractmethod
def _enter_state(self, new_state: Union[str, Enum], old_state: Union[str, Enum]):
pass

@abstractmethod
def _exit_state(self, old_state: Union[str, Enum], new_state: Union[str, Enum]):
pass

def set_state(self, new_state: Union[str, Enum]):
self._previous_state = self._state
self._state = new_state
if self._previous_state:
self._exit_state(self._previous_state, new_state)
if new_state:
self._enter_state(new_state, self._previous_state)

def add_state(self, state: Union[str, Enum]):
self._states[state] = state
self._transitions[state] = []
return self

def add_transition(self, from_state: Union[str, Enum], to_state: Union[str, Enum], condition_func: Callable[[], bool]):
self._transitions[from_state].append(Transition(from_state, to_state, condition_func))
return self

def tick(self):
if self._state:
self._state_logic()
transition = self._get_transition()
if transition:
self.set_state(transition)
7 changes: 3 additions & 4 deletions src/objects/npc.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import random

from transitions import Machine

from src.common.state_machine import StateMachine
from src.common.point import Point
from src.objects.object import Object
from src.actions.move import Move
Expand All @@ -13,10 +12,10 @@ class NPC(Object):
"""
NPC interface.
"""
machine: Machine
machine: StateMachine
hp: int

def __init__(self, scene: 'Scene', position: Point, hp: int, machine: Machine) -> None:
def __init__(self, scene: 'Scene', position: Point, hp: int, machine: StateMachine) -> None:
super().__init__(position, scene)
self.machine = machine
self.hp = hp
Expand Down
86 changes: 86 additions & 0 deletions src/objects/npcs/enemies/bear.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from typing import List
from enum import Enum

from src.common.state_machine import StateMachine
from src.common.enums import Direction
from src.objects.npcs.enemy import Enemy
from src.objects.agent import Agent
from src.common.point import Point
from src.actions.prepare_to_slam import PrepareToSlam
from src.actions.attacks.slam import Slam

HP = 100


class Bear(Enemy):
"""
Bear boss.
"""

def __init__(self, scene: 'Scene', position: Point) -> None:
super().__init__(scene, position, HP, Bear.BearStateMachine(self))

def occupied_fields(self, current_position: Point = None) -> List[Point]:
if not current_position:
current_position = self.properties.position
return [current_position,
current_position + Point(0, 1),
current_position + Point(1, 1),
current_position + Point(1, 0)]

def tick(self) -> None:
self.machine.tick()

def slam(self):
Slam(self, self.fields_around()).execute()

def prepare_to_slam(self):
PrepareToSlam(self).execute()

def is_there_agent_to_slam(self):
for target_position in self.fields_around():
if not self.scene._objects_position_map.get(target_position):
continue
for target in self.scene.get_objects_by_position(target_position):
if isinstance(target, Agent):
return True
return False

class State(Enum):
IDLE = 0
MOVE = 1
PREPARE_TO_SLAM = 2
SLAM = 3

class BearStateMachine(StateMachine):

def __init__(self, bear: 'Bear'):
super().__init__()
self.bear = bear
self.add_state(Bear.State.IDLE)\
.add_state(Bear.State.MOVE)\
.add_state(Bear.State.PREPARE_TO_SLAM)\
.add_state(Bear.State.SLAM)\
.add_transition(Bear.State.IDLE, Bear.State.PREPARE_TO_SLAM, lambda: self.bear.is_there_agent_to_slam())\
.add_transition(Bear.State.IDLE, Bear.State.MOVE, lambda: True)\
.add_transition(Bear.State.MOVE, Bear.State.PREPARE_TO_SLAM, lambda: self.bear.is_there_agent_to_slam())\
.add_transition(Bear.State.PREPARE_TO_SLAM, Bear.State.SLAM, lambda: True)\
.add_transition(Bear.State.SLAM, Bear.State.IDLE, lambda: True)\
.set_state(Bear.State.IDLE)

def _state_logic(self):
match self._state:
case Bear.State.IDLE:
self.bear.idle()
case Bear.State.MOVE:
self.bear.move()
case Bear.State.PREPARE_TO_SLAM:
self.bear.prepare_to_slam()
case Bear.State.SLAM:
self.bear.slam()

def _enter_state(self, new_state, old_state):
pass

def _exit_state(self, old_state, new_state):
pass
26 changes: 20 additions & 6 deletions src/objects/npcs/enemies/training_dummy.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
from transitions import Machine, State

from src.common.state_machine import StateMachine
from src.objects.npcs.enemy import Enemy
from src.common.point import Point

TRAINING_DUMMY_HP = 1000000
TRAINING_DUMMY_STATES = [State(name='idle')]


class TrainingDummy(Enemy):
Expand All @@ -13,8 +11,24 @@ class TrainingDummy(Enemy):
"""

def __init__(self, scene: 'Scene', position: Point) -> None:
super().__init__(scene, position, TRAINING_DUMMY_HP,
Machine(model=self, states=TRAINING_DUMMY_STATES, initial='idle'))
super().__init__(scene, position, TRAINING_DUMMY_HP, TrainingDummyStateMachine(self))

def tick(self) -> None:
self.idle()
self.machine.tick()


class TrainingDummyStateMachine(StateMachine):
def __init__(self, dummy: TrainingDummy):
super().__init__()
self.dummy = dummy
self.add_state("idle")\
.set_state("idle")

def _state_logic(self):
self.dummy.idle()

def _enter_state(self, new_state, old_state):
pass

def _exit_state(self, old_state, new_state):
pass
5 changes: 2 additions & 3 deletions src/objects/npcs/enemy.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from transitions import Machine

from src.common.state_machine import StateMachine
from src.objects.npc import NPC
from src.common.point import Point

Expand All @@ -9,5 +8,5 @@ class Enemy(NPC):
Enemy interface.
"""

def __init__(self, scene: 'Scene', position: Point, hp: int, machine: Machine) -> None:
def __init__(self, scene: 'Scene', position: Point, hp: int, machine: StateMachine) -> None:
super().__init__(scene, position, hp, machine)
25 changes: 18 additions & 7 deletions src/objects/object.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from typing import List

from src.common.point import Point
from src.common.enums import Direction
from src.common.serializable import Serializable


Expand All @@ -25,13 +28,21 @@ def __init__(self, position: Point, scene: 'Scene') -> None:
# Log creation of the `Object`
print(self)

def occupy_field(self, position: Point) -> bool:
alpinus4 marked this conversation as resolved.
Show resolved Hide resolved
self.scene.get_objects_by_position(position).append(self)
return True

def free_field(self, position: Point) -> None:
self.scene.get_objects_by_position(position).remove(self)
return True
def occupied_fields(self, current_position: Point = None) -> List[Point]:
if not current_position:
current_position = self.properties.position
return [current_position]

def fields_around(self) -> List[Point]:
fields = []
for field in self.occupied_fields():
points_around_field = [field + Direction.NORTH.value,
field + Direction.WEST.value,
field + Direction.SOUTH.value,
field + Direction.EAST.value]
# append all points around that aren't in self.occupied_fields()
fields.extend(list(filter(lambda point: point not in self.occupied_fields(), points_around_field)))
return fields

def tick(self) -> None:
"""
Expand Down
Loading