Using async for transition conds #525
-
|
Greetings there, Hope all are well. I saw in some of the examples the cond methods are implemented as async. I was wondering when one should opt for that, and when it's not needed, or should not be used. For context, here is my current fsm: from __future__ import annotations
__all__ = ["SoldierFSM"]
import random
from statemachine import StateMachine, State
class SoldierFSM(StateMachine):
""" `banjo.characters.SoldierFSM` is a state machine that defines the
different states and transitions of a soldier character in a game.
The soldier can be in one of the following states:
- Idle: The soldier is not doing anything. We start in this state.
- Walking: The soldier is walking.
- Running: The soldier is running.
- Shooting: The soldier is shooting at an enemy.
- Reloading: The soldier is reloading their weapon.
- Melee: The soldier is attacking an enemy with a melee weapon.
- Hurting: The soldier is being hit by an enemy.
- Dying: The soldier is dying. We cannot exit this state.
The soldier can transition between these states based on certain conditions.
The transitions are defined in the `cycle` variable, which is a combination
of the different states and their conditions.
Different conditions will define the behavior of the soldier in the game,
such as whether they would prioritize certain actions over others.
Parameters
----------
`hp` : int, optional, default=30
The soldier's health points.
`shooting_range` : int, optional, default=800
The range of the soldier's shooting.
`melee_range` : int, optional, default=100
The range of the soldier's melee attack.
`shooting_damage` : int, optional, default=10
The damage dealt by the soldier's shooting.
`melee_damage` : int, optional, default=20
The damage dealt by the soldier's melee attack.
`shooting_accuracy` : float, optional, default=0.6
The accuracy of the soldier's shooting.
`mag_size` : int, optional, default=20
The size of the soldier's magazine.
Attributes
----------
`hp` : int
The soldier's health points.
`shooting_range` : int
The range of the soldier's shooting.
`melee_range` : int
The range of the soldier's melee attack.
`shooting_damage` : int
The damage dealt by the soldier's shooting.
`melee_damage` : int
The damage dealt by the soldier's melee attack.
`shooting_accuracy` : float
The accuracy of the soldier's shooting.
`mag_size` : int
The size of the soldier's magazine.
`current_mag` : int
The current number of bullets in the soldier's magazine.
`patrol_checkpoints` : list[int]
The list of patrol checkpoints for the soldier.
`chase_to` : int | None
The coordinate the soldier is chasing to. When nothing is being chased,
this will be None.
`take_a_break` : bool
Whether the soldier is taking a break or not.
Usage
-----
>>> soldier = SoldierFSM()
"""
idle = State("Idle", initial=True)
walking = State("Walking")
running = State("Running")
shooting = State("Shooting")
reloading = State("Reloading")
melee = State("Melee")
hurting = State("Hurting")
dying = State("Dying", final=True)
# Transitions are evaluated in the order which
# they are defined
# Use the order to define precedence
cycle = \
idle.to(hurting, cond="is_being_hit") | \
idle.to(melee, cond="can_melee") | \
idle.to(reloading, cond="mag_empty") | \
idle.to(shooting, cond="can_shoot") | \
idle.to(running, cond="can_chase") | \
idle.to(walking, cond="can_patrol") | \
idle.to(idle) | \
walking.to(hurting, cond="is_being_hit") | \
walking.to(melee, cond="can_melee") | \
walking.to(reloading, cond="mag_empty") | \
walking.to(shooting, cond="can_shoot") | \
walking.to(running, cond="can_chase") | \
walking.to(walking, cond="can_patrol") | \
walking.to(idle, cond="can_take_a_break") | \
running.to(hurting, cond="is_being_hit") | \
running.to(melee, cond="can_melee") | \
running.to(reloading, cond="mag_empty") | \
running.to(shooting, cond="can_shoot") | \
running.to(running, cond="can_chase") | \
running.to(walking, cond="can_patrol") | \
running.to(idle) | \
shooting.to(hurting, cond="is_being_hit") | \
shooting.to(melee, cond="can_melee") | \
shooting.to(reloading, cond="mag_empty") | \
shooting.to(shooting, cond="can_shoot") | \
shooting.to(running, cond="can_chase") | \
shooting.to(idle) | \
melee.to(hurting, cond="is_being_hit") | \
melee.to(melee, cond="can_melee") | \
melee.to(reloading, cond=["mag_empty", "can_shoot"]) | \
melee.to(shooting, cond="can_shoot") | \
melee.to(running, cond="can_chase") | \
melee.to(idle) | \
reloading.to(hurting, cond="is_being_hit") | \
reloading.to(melee, cond="can_melee") | \
reloading.to(reloading, cond="mag_empty") | \
reloading.to(shooting, cond="can_shoot") | \
reloading.to(running, cond="can_chase") | \
reloading.to(idle) | \
hurting.to(dying, cond="is_dying") | \
hurting.to(hurting, cond="is_being_hit")
def __init__(
self,
hp: int = 30,
shooting_range: int = 800,
melee_range: int = 100,
shooting_damage: int = 10,
melee_damage: int = 20,
shooting_accuracy: float = 0.6,
mag_size: int = 20
) -> None:
""" Initialize a `banjo.characters.SoldierFSM` instance.
"""
# Soldier specific attributes
self.hp = hp
self.shooting_range = shooting_range
self.melee_range = melee_range
self.shooting_damage = shooting_damage
self.melee_damage = melee_damage
self.shooting_accuracy = shooting_accuracy
self.mag_size = mag_size
# General attributes
self.current_mag = self.mag_size
self.patrol_checkpoints = [1, 2, 4]
self.chase_to = None
self.take_a_break = False
self.current_coordinate = 0
super().__init__()
def can_take_a_break(self) -> bool:
""" Check if the soldier can take a break.
Returns
-------
bool
True if the soldier can take a break, False otherwise.
"""
if self.take_a_break:
self.take_a_break = False
return True
return False
def can_patrol(self) -> bool:
""" Check if the soldier can patrol.
Returns
-------
bool
True if the soldier can patrol, False otherwise.
"""
if self.current_coordinate < self.patrol_checkpoints[-1]:
self.current_coordinate += 1
return True
elif self.current_coordinate > self.patrol_checkpoints[-1]:
self.current_coordinate -= 1
return True
self.take_a_break = True
# Remove the last checkpoint from the list
# This makes the patrol less predictable
self.patrol_checkpoints.pop()
new_coordinate = random.randint(0, 5)
# Coordinate must be unique, to avoid looping taking a break
# at the same checkpoint
while new_coordinate in self.patrol_checkpoints:
new_coordinate = random.randint(0, 5)
self.patrol_checkpoints.insert(0, new_coordinate)
return False
def can_chase(self) -> bool:
""" Check if the soldier can chase an enemy.
Returns
-------
bool
True if the soldier can chase an enemy, False otherwise.
"""
if self.chase_to is not None:
if self.current_coordinate < self.chase_to:
self.current_coordinate += 1
return True
elif self.current_coordinate > self.chase_to:
self.current_coordinate -= 1
return True
# Reset when nothing is being chased
self.chase_to = None
return True
return False
def can_shoot(
self,
enemy_distance: int,
enemy_coordinate: int
) -> bool:
""" Check if the soldier can shoot an enemy.
Parameters
----------
`enemy_distance` : int
The distance to the enemy.
`enemy_coordinate` : int
The coordinate of the enemy.
Returns
-------
`can_shoot` : bool
True if the soldier can shoot the enemy, False otherwise.
"""
can_shoot = enemy_distance <= self.shooting_range and enemy_distance > self.melee_range
if can_shoot:
self.current_mag -= 1
self.chase_to = enemy_coordinate
return can_shoot
def can_melee(
self,
enemy_distance: int,
enemy_coordinate: int
) -> bool:
""" Check if the soldier can melee an enemy.
Parameters
----------
`enemy_distance` : int
The distance to the enemy.
`enemy_coordinate` : int
The coordinate of the enemy.
Returns
-------
`can_melee` : bool
True if the soldier can melee the enemy, False otherwise.
"""
can_melee = enemy_distance <= self.melee_range
if can_melee:
self.chase_to = enemy_coordinate
return can_melee
def mag_empty(self) -> bool:
""" Check if the soldier's magazine is empty.
Returns
-------
bool
True if the soldier's magazine is empty, False otherwise.
"""
if self.current_mag == 0:
self.current_mag = self.mag_size
return True
return False
def is_being_hit(
self,
enemy_hitting: bool,
enemy_damage: int
) -> bool:
""" Check if the soldier is being hit by an enemy.
Parameters
----------
`enemy_hitting` : bool
Whether the enemy is hitting the soldier or not.
`enemy_damage` : int
The damage dealt by the enemy.
Returns
-------
bool
True if the soldier is being hit, False otherwise.
"""
if enemy_hitting:
self.hp -= enemy_damage
return True
return False
def is_dying(self) -> bool:
""" Check if the soldier is dying.
Returns
-------
bool
True if the soldier is dying, False otherwise.
"""
if self.hp <= 0:
return True
return False |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 2 replies
-
|
Further context. I want to use this to manage my NPC states in I imagine it would be okay for |
Beta Was this translation helpful? Give feedback.
@ACE07-Sev as I understand, you only need async if you want to use
asyncioin your callback. I.e. if you want toawaita coroutine.Since you don't use
asyncioin your code, there is no need for async callbacks.