Skip to content

Commit

Permalink
Add Hunter (Thợ săn) character (#143)
Browse files Browse the repository at this point in the history
* Add hunter document

* Handle Hunter code

* Add hunter test cases


---------

Co-authored-by: HUYPC\Huy Bui <[email protected]>
  • Loading branch information
nitro2 and huybt22 authored Mar 14, 2024
1 parent 20dc2db commit bd0e0bf
Show file tree
Hide file tree
Showing 18 changed files with 577 additions and 17 deletions.
1 change: 1 addition & 0 deletions STORY_VN.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Vậy bên nào sẽ chiến thắng, Thường dân, Werewolf, hay là phe Ph
- [Seer] Tiên tri: Soi một người chơi có phải là sói hay không. Có thể giết chết Cáo nếu soi trúng Cáo.
- [Guard] Bảo vệ: Bảo vệ được chọn 1 người khác nhau mỗi đêm trừ bản thân và người được chọn sẽ bất tử đêm đó.
- [Lycan] Người hóa sói: Người hóa sói thuộc Phe dân làng, nhưng nếu được chỉ định bởi Tiên tri, thì sẽ bị thông báo là Sói.
- [Hunter] Thợ săn: Vào ban đêm, được chọn một người chơi khác để chết chung nếu bản thân bị giết.

### II. Phe sói: Chiến thắng nếu giết hết dân làng. Nhận biết được **sói** cùng phe.

Expand Down
2 changes: 1 addition & 1 deletion commands/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ async def parse_command(client, game, message):
if not game.is_started():
await message.reply(text_templates.generate_text("game_not_started_text"))
return
if message.channel.name not in (config.GAMEPLAY_CHANNEL, config.LOBBY_CHANNEL): # Only use in common channels, no spamming
if message.channel.name not in (config.GAMEPLAY_CHANNEL, config.LOBBY_CHANNEL): # Only use in common channels, no spamming
await message.reply(text_templates.generate_text("invalid_channel_text", channel=f"#{config.LOBBY_CHANNEL} #{config.GAMEPLAY_CHANNEL}"))
return
msg = await game.self_check_channel()
Expand Down
50 changes: 48 additions & 2 deletions game/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,8 @@ async def delete_channel(self):
async def self_check_channel(self):
try:
await asyncio.gather(
*[player.create_personal_channel(self_check=True) for player in self.players.values()]
)
*[player.create_personal_channel(self_check=True) for player in self.players.values()]
)

return text_templates.generate_text('self_check_text')
except Exception as e:
Expand Down Expand Up @@ -611,6 +611,17 @@ async def do_end_daytime_phase(self):
"couple_died_on_day_text", config.GAMEPLAY_CHANNEL,
died_player=f"<@{lynched}>", follow_player=f"<@{cupid_couple}>"
)
# Kill anyone who is hunted if hunter dies with his couple
hunted = await self.get_hunted_target_on_hunter_death(cupid_couple)
if hunted:
await self.interface.send_action_text_to_channel(
"hunter_killed_text", config.GAMEPLAY_CHANNEL, target=f"<@{hunted}>"
)
hunted = await self.get_hunted_target_on_hunter_death(lynched)
if hunted:
await self.interface.send_action_text_to_channel(
"hunter_killed_text", config.GAMEPLAY_CHANNEL, target=f"<@{hunted}>"
)
else:
await self.interface.send_action_text_to_channel("execution_none_text", config.GAMEPLAY_CHANNEL)

Expand Down Expand Up @@ -665,6 +676,14 @@ async def do_end_nighttime_phase(self):
final_kill_list.append(_id)
if self.cupid_dict.get(_id):
cupid_couple = self.cupid_dict[_id]
hunted = await self.get_hunted_target_on_hunter_death(cupid_couple)
if hunted:
final_kill_list.append(hunted)

# Kill anyone who is hunted if hunter is killed
hunted = await self.get_hunted_target_on_hunter_death(_id)
if hunted:
final_kill_list.append(hunted)

kills = ", ".join(f"<@{_id}>" for _id in final_kill_list)
self.night_pending_kill_list = [] # Reset killed list for next day
Expand Down Expand Up @@ -844,6 +863,8 @@ async def do_player_action(self, cmd, author_id, *targets_id):
return await self.kill(author, targets[0])
if cmd == "guard":
return await self.guard(author, targets[0])
if cmd == "hunt":
return await self.hunt(author, targets[0])
if cmd == "seer":
return await self.seer(author, targets[0])
if cmd == "reborn":
Expand Down Expand Up @@ -1016,6 +1037,31 @@ async def ship(self, author, target1, target2):

return text_templates.generate_text("cupid_after_ship_text", target1=f"<@{target1_id}>", target2=f"<@{target2_id}>")

async def hunt(self, author, target):
if not isinstance(author, roles.Hunter):
return text_templates.generate_text("invalid_author_text")

if self.game_phase != const.GamePhase.NIGHT:
return text_templates.generate_text("invalid_nighttime_text")

# author_id = author.player_id
target_id = target.player_id

if not target.is_alive():
return text_templates.generate_text("invalid_hunter_target_text", user=f"<@{target_id}>")

author.set_hunted_target(target_id)
return text_templates.generate_text("hunter_after_voting_text", target=f"<@{target_id}>")

# Kill anyone who is hunted
async def get_hunted_target_on_hunter_death(self, hunter):
if isinstance(self.players[hunter], roles.Hunter):
hunted = self.players[hunter].get_hunted_target()
if hunted and hunted != hunter:
if await self.players[hunted].get_killed():
return hunted
return None

async def register_auto(self, author, subcmd):
def check(pred):
def wrapper(f):
Expand Down
3 changes: 2 additions & 1 deletion game/roles/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
from game.roles.zombie import Zombie
from game.roles.cupid import Cupid
from game.roles.chief import Chief
from game.roles.hunter import Hunter
import utils


role_info = utils.common.read_json_file("json/role_info.json")


def get_all_roles():
return Villager, Werewolf, Seer, Guard, Lycan, Betrayer, Superwolf, Fox, Witch, Zombie, Cupid, Chief
return Villager, Werewolf, Seer, Guard, Lycan, Betrayer, Superwolf, Fox, Witch, Zombie, Cupid, Chief, Hunter


def get_role_type(name):
Expand Down
2 changes: 1 addition & 1 deletion game/roles/character.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def on_use_mana(self):
def get_mana(self):
return self.mana

async def create_personal_channel(self, self_check = False):
async def create_personal_channel(self, self_check=False):
await self.interface.create_channel(self.channel_name)
await self.interface.add_user_to_channel(self.player_id, self.channel_name, is_read=True, is_send=True)
if not self_check:
Expand Down
19 changes: 19 additions & 0 deletions game/roles/hunter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from game.roles.villager import Villager


class Hunter(Villager):
''' Hunter is basic Villager with ability to kill anyone on his death (die with him) '''

def __init__(self, interface, player_id, player_name):
super().__init__(interface, player_id, player_name)
self.hunted_target = None

async def on_action(self, embed_data):
await self.interface.send_action_text_to_channel("hunter_before_voting_text", self.channel_name)
await self.interface.send_embed_to_channel(embed_data, self.channel_name)

def get_hunted_target(self):
return self.hunted_target

def set_hunted_target(self, hunted):
self.hunted_target = hunted
2 changes: 1 addition & 1 deletion game/text_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def generate_id_player_list(player_list, alive_status, reveal_role=False):
for user in player_list:
if alive_status is None and user.status == CharacterStatus.KILLED:
# Handle for dead players in All list
id_player_list.append(f"💀 -> <@{user.player_id}>" + (f" - {user.get_role()}" if reveal_role else "")) # Do not increase row_id when user is dead
id_player_list.append(f"💀 -> <@{user.player_id}>" + (f" - {user.get_role()}" if reveal_role else "")) # Do not increase row_id when user is dead
else:
# Show player id for: alive players in All list, Alive list; dead players in Dead list.
# Also show role info if it's a dead list reveal mode is enabled.
Expand Down
8 changes: 8 additions & 0 deletions json/command_info.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@
"exclusive_roles": ["Guard"],
"required_params": ["player_id"]
},
"hunt": {
"description": {
"vi": "Săn một ai đó.",
"en": "Hunt a player."
},
"exclusive_roles": ["Hunter"],
"required_params": ["player_id"]
},
"seer": {
"description": {
"vi": "Soi một người bất kỳ.",
Expand Down
18 changes: 10 additions & 8 deletions json/role_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,28 @@
{"Werewolf": 1, "Seer": 1, "Guard": 1, "Witch": 1, "Cupid": 1, "Fox": 1, "Betrayer": 1},
{"Werewolf": 2, "Seer": 1, "Guard": 1, "Zombie": 1, "Cupid": 1 , "Witch": 1},
{"Werewolf": 1, "Seer": 1, "Guard": 1, "Chief": 1, "Witch": 1, "Cupid": 1, "Fox": 1, "Lycan": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Villager": 1, "Witch": 1, "Cupid": 1, "Chief": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Hunter": 1, "Witch": 1, "Cupid": 1, "Chief": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Villager": 3, "Witch": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Witch": 1, "Villager": 1, "Fox": 1, "Lycan": 1},
{"Werewolf": 2, "Seer": 1, "Guard": 1, "Villager": 1, "Witch" : 1, "Cupid": 1, "Chief": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Witch": 1, "Hunter": 1, "Fox": 1, "Lycan": 1},
{"Werewolf": 2, "Seer": 1, "Guard": 1, "Hunter": 1, "Witch" : 1, "Cupid": 1, "Chief": 1},
{"Werewolf": 2, "Seer": 1, "Guard": 1, "Chief": 1, "Witch": 1, "Cupid": 1, "Fox": 1, "Zombie": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Zombie": 1, "Fox": 1, "Lycan": 1, "Cupid": 1, "Witch": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Villager": 2, "Fox": 1, "Witch": 1, "Chief": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Villager": 2, "Fox": 1, "Cupid": 1, "Chief": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Villager": 1, "Fox": 1, "Witch": 1, "Chief": 1, "Hunter": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Villager": 1, "Fox": 1, "Cupid": 1, "Chief": 1, "Hunter":1},
{"Werewolf": 3, "Seer": 1, "Guard": 1, "Witch": 1, "Villager": 3, "Cupid": 1, "Chief": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Zombie": 1, "Fox": 1, "Chief": 1, "Cupid": 1, "Witch": 1, "Betrayer":1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Zombie": 1, "Fox": 1, "Lycan": 1, "Cupid": 1, "Witch": 1, "Betrayer":1, "Chief": 1},
{"Werewolf": 2, "Seer": 1, "Guard": 1, "Villager": 2, "Witch": 1, "Fox": 1, "Lycan": 1, "Cupid": 1, "Chief": 1},
{"Werewolf": 3, "Seer": 1, "Guard": 1, "Villager": 1, "Witch": 1, "Fox": 1, "Lycan": 1, "Cupid": 1, "Chief": 1},
{"Superwolf": 1, "Werewolf": 2, "Seer": 1, "Guard": 1, "Villager": 2, "Witch": 1, "Cupid": 1, "Fox": 1, "Chief": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Villager": 6, "Lycan": 1},
{"Werewolf": 3, "Seer": 1, "Guard": 1, "Hunter": 1, "Witch": 1, "Fox": 1, "Lycan": 1, "Cupid": 1, "Chief": 1},
{"Superwolf": 1, "Werewolf": 2, "Seer": 1, "Guard": 1, "Villager": 1, "Witch": 1, "Cupid": 1, "Fox": 1, "Chief": 1, "Hunter": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Villager": 3, "Lycan": 2, "Witch": 1, "Cupid": 1},
{"Werewolf": 2, "Seer": 1, "Guard": 1, "Fox": 1, "Witch": 1, "Villager": 4, "Lycan": 1},
{"Superwolf": 1, "Werewolf": 2, "Seer": 1, "Guard": 1, "Villager": 4, "Witch": 1, "Cupid": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Fox": 1, "Witch": 1, "Villager": 1, "Lycan": 2, "Cupid": 1, "Betrayer": 1, "Zombie": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Fox": 1, "Witch": 1, "Villager": 1, "Lycan": 2, "Cupid": 1, "Betrayer": 1, "Zombie": 1, "Hunter": 1},
{"Werewolf": 3, "Seer": 1, "Guard": 1, "Witch": 1, "Villager": 4, "Lycan": 1, "Cupid": 1},
{"Werewolf": 3, "Seer": 1, "Guard": 1, "Witch": 1, "Villager": 4, "Lycan": 1, "Cupid": 1, "Hunter": 1},
{"Werewolf": 3, "Seer": 1, "Guard": 1, "Witch": 1, "Villager": 4, "Lycan": 1, "Cupid": 1, "Hunter": 1, "Chief": 1},
{"Superwolf": 1, "Werewolf": 2, "Seer": 1, "Guard": 1, "Villager": 5, "Lycan": 1, "Cupid": 1, "Witch": 1},
{"Superwolf": 1, "Werewolf": 2, "Seer": 1, "Guard": 1, "Fox": 1, "Witch": 1, "Villager": 4, "Lycan": 1, "Cupid": 1},
{"Superwolf": 1, "Werewolf": 2, "Seer": 1, "Guard": 1, "Witch": 1, "Villager": 5, "Lycan": 1, "Fox": 1, "Cupid": 1}
Expand Down
10 changes: 10 additions & 0 deletions json/role_info.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@
"daytime_command": "vote",
"nighttime_commands": ["guard"]
},
"Hunter": {
"name_vi": "Thợ săn",
"party": "Villager",
"description": {
"vi": "Vào ban đêm, chọn 1 người khác để chết chung với mình. Ghi nhận lệnh cuối cùng.",
"en": "Target a player to die together at night. Accept latest command."
},
"daytime_command": "vote",
"nighttime_commands": ["hunt"]
},
"Lycan": {
"name_vi": "Người hóa sói",
"party": "Villager",
Expand Down
36 changes: 36 additions & 0 deletions json/text_template.json
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,42 @@
"en": ["You protected this person yesterday. Let's change the target today!"]
}
},
"hunter_before_voting_text": {
"params": [],
"template": {
"vi": [
"🔫 Bạn muốn ai chết chung với mình? Hãy nhập `{bot_prefix}hunt ID` để chọn đối tượng.",
"💡 Ví dụ: `{bot_prefix}hunter 2`",
"⚠️ Bạn được sử dụng kỹ năng này bất kỳ lúc nào, có thể dùng lên bản thân nếu đổi ý. Nhưng đừng giết bậy bạn nhé!"
],
"en": [
"🔫 Choose someone to die with, enter `{bot_prefix}hunt ID` to let that person die with you.",
"💡 Example: `{bot_prefix}hunt 2`",
"⚠️ You can use this skill anytime! Tagert yourself if you change your mind."
]
}
},
"hunter_after_voting_text": {
"params": ["target"],
"template": {
"vi": ["🔫 Thợ săn đã xác định đối tượng thành công {target}"],
"en": ["🔫 Hunter has targeted victim {target}"]
}
},
"invalid_hunter_target_text": {
"params": [],
"template": {
"vi": ["Người ta đã chết rồi còn cố giết nữa 😡"],
"en": ["Don't target dead person 😡"]
}
},
"hunter_killed_text": {
"params": ["target"],
"template": {
"vi": ["🔫 Trước khi chết, thợ săn bắn phát súng cuối cùng vào {target}"],
"en": ["🔫 Before dying, the hunter shot {target}"]
}
},
"witch_before_voting_text": {
"params": [],
"template": {
Expand Down
9 changes: 6 additions & 3 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,12 @@ async def test_game():
game = Game(None, interface.ConsoleInterface(None))

# Run single test

await test_case(game, "testcases/case-chief-vote-break-draw.json")

await test_case(game, "testcases/case-hunter-couple-die-together-by-kill.json")
await test_case(game, "testcases/case-hunter-couple-die-together-by-vote.json")
await test_case(game, "testcases/case-hunter-hunt-fox.json")
await test_case(game, "testcases/case-hunter-hunt-wolf.json")
await test_case(game, "testcases/case-hunter-simple.json")
await test_case(game, "testcases/case-hunter-hunt-night1.json")
# Run all tests
directory = "testcases"
for filename in os.listdir(directory):
Expand Down
74 changes: 74 additions & 0 deletions testcases/case-hunter-couple-die-together-by-kill.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"name": "Hunter die together by kill",
"player_list": {
"w1": "Werewolf",
"s1": "Seer",
"h1": "Hunter",
"c1": "Cupid",
"sw1": "Superwolf",
"wi1": "Witch"
},
"timeline": [
{
"time": "day1",
"alive": [
"w1",
"h1",
"c1",
"sw1",
"wi1",
"s1"
],
"action": [
"w1 vote h1",
"h1 vote w1",
"c1 ship h1 s1",
"sw1 vote c1",
"wi1 vote sw1"
]
},
{
"time": "night1",
"alive": [
"w1",
"s1",
"h1",
"c1",
"wi1",
"sw1"
],
"action": [
"w1 kill wi1",
"s1 seer h1",
"h1 hunt c1"
]
},
{
"time": "day2",
"alive": [
"w1",
"s1",
"h1",
"c1",
"sw1"
],
"action": [
"w1 vote sw1",
"c1 vote sw1"
]
},
{
"time": "night2",
"alive": [
"w1",
"s1",
"h1",
"c1"
],
"action": [
"w1 kill s1"
]
}
],
"win": "Werewolf"
}
Loading

0 comments on commit bd0e0bf

Please sign in to comment.