Skip to content

Commit

Permalink
Add Harlot role (#267)
Browse files Browse the repository at this point in the history
* create help text

* add Harlot text template

* add testcases for Harlot

* create + add action Harlot roles class

* generate Harlot role
  • Loading branch information
neihousaigaai authored Aug 25, 2024
1 parent 64f58c0 commit b358aea
Show file tree
Hide file tree
Showing 24 changed files with 1,056 additions and 22 deletions.
1 change: 1 addition & 0 deletions STORY_VN.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Vậy bên nào sẽ chiến thắng, Thường dân, Werewolf, hay là phe Ph
- [Diseased] Người bệnh: Nếu Người bệnh bị Sói cắn, đàn Sói sẽ không thể cắn người nào vào đêm tiếp theo do đã bị lây bệnh.
- [ApprenticeSeer] Tiên tri tập sự: Nếu Tiên tri đã chết, Tiên tri tập sự sẽ trở thành Tiên tri của làng.
- [Cursed] Kẻ bị Nguyền rủa: Thuộc phe dân làng, nhưng nếu bị Sói cắn, Kẻ bị Nguyền rủa sẽ gia nhập vào phe sói, chống lại dân làng.
- [Harlot] Kỹ nữ: Kỹ nữ được lựa chọn một người để đến ngủ thăm vào mỗi đêm. Nếu Kỹ nữ đến thăm nạn nhân của Sói, Kỹ nữ sẽ chết theo kẻ đó. Nếu Kỹ nữ đến thăm Sói, Sói sẽ không cắn được ai và Kỹ nữ sẽ chết. Nếu Kỹ nữ bị Sói nhắm trúng mà Kỹ nữ đi thăm người khác, Kỹ nữ sẽ không bị sói cắn.

### 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 @@ -118,7 +118,7 @@ async def do_game_cmd(game, message, cmd, parameters, force=False):
return
await player.do_rematch(game, message)

elif cmd in ("vote", "punish", "kill", "guard", "seer", "hunter", "reborn", "curse", "zombie", "ship", "auto", "autopsy", "bite"):
elif cmd in ("vote", "punish", "kill", "guard", "seer", "hunter", "reborn", "curse", "zombie", "ship", "auto", "autopsy", "bite", "sleep"):
try:
await player.do_character_cmd(game, message, cmd, parameters)
except Exception as e:
Expand Down
69 changes: 58 additions & 11 deletions game/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,6 @@ def generate_player_list_embed(self, alive_status=None):

if player_list:
id_player_list = text_template.generate_id_player_list(player_list, alive_status, reveal_role)
print("generate_player_list_embed", alive_status, reveal_role, player_list, id_player_list)
embed_data = text_templates.generate_embed(action_name, [id_player_list, role_list])
return embed_data
return None
Expand Down Expand Up @@ -979,6 +978,8 @@ async def do_end_nighttime_phase(self):
await self.pathologist_do_end_nighttime_phase(player)
elif isinstance(player, roles.Rat):
await self.rat_do_end_nighttime_phase(player)
elif isinstance(player, roles.Harlot):
await self.harlot_do_end_nighttime_phase(player)

await self.werewolf_do_end_nighttime_phase()

Expand All @@ -1002,7 +1003,8 @@ async def do_end_nighttime_phase(self):
self.reborn_set = set()

async def send_status_changes_info_on_end_phase(self, kills_list_by_reason, **kw_info):
for reason, id_list in kills_list_by_reason.items():
for reason in sorted(kills_list_by_reason, key=lambda r: (r.value > 0, abs(r.value))):
id_list = kills_list_by_reason[reason]
label = reason.get_template_label(self.game_phase)

if reason is const.DeadReason.HIDDEN:
Expand All @@ -1013,7 +1015,7 @@ async def send_status_changes_info_on_end_phase(self, kills_list_by_reason, **kw
)
else:
for _id in id_list:
if reason is const.DeadReason.TANNER_NO_VOTE:
if reason is const.DeadReason.TANNER_NO_VOTE or reason is const.DeadReason.SLEPT_OVER:
kwargs = {"user": f"<@{_id}>"}
elif reason is const.DeadReason.LYNCHED:
kwargs = {"voted_user": f"<@{_id}>", "highest_vote_number": kw_info.get("highest_vote_number", 0)}
Expand Down Expand Up @@ -1057,7 +1059,7 @@ async def seer_do_end_nighttime_phase(self, author):
self.night_pending_kill_list.append((target_id, const.DeadReason.HIDDEN))

if self.new_moon_mode.get_current_event() is Somnambulism:
await self.new_moon_mode.do_end_nighttime_phase(self.interface, target=target)
await self.new_moon_mode.do_end_nighttime_phase(self.interface, target_role=target.get_role())

await author.send_to_personal_channel(
text_templates.generate_text(
Expand Down Expand Up @@ -1123,6 +1125,26 @@ async def rat_do_end_nighttime_phase(self, author):
if not target.is_protected():
self.night_pending_kill_list.append((author.player_id, const.DeadReason.HIDDEN))

async def harlot_do_end_nighttime_phase(self, author):
if not author.is_alive():
return

target_id = author.get_target()
if target_id is None or target_id == author.player_id:
return

target = self.players[target_id]

if Game.is_role_in_werewolf_party(target):
self.night_pending_kill_list.append((author.player_id, const.DeadReason.SLEPT_OVER))

elif isinstance(target, (roles.Diseased, roles.Cursed)):
target.set_action_disabled_today(True)

await author.send_to_personal_channel(
text_templates.generate_text("harlot_result_text", target=f"<@{target_id}>")
)

async def werewolf_do_end_nighttime_phase(self):
if self.is_werewolf_diseased:
self.is_werewolf_diseased = False
Expand All @@ -1134,19 +1156,39 @@ async def werewolf_do_end_nighttime_phase(self):
self.wolf_kill_dict = {}

if killed:
await self.interface.send_action_text_to_channel(
"werewolf_kill_result_text", config.WEREWOLF_CHANNEL, target=f"<@{killed}>"
)
# check Harlot case
harlot_id = self.get_player_with_role(roles.Harlot)
harlot_target_id = None
if harlot_id:
harlot_target_id = self.players[harlot_id].get_target()
if harlot_target_id == harlot_id:
harlot_target_id = None

if killed == harlot_id and (harlot_target_id and harlot_target_id != harlot_id):
# if Harlot is Werewolf's victim but Harlot was visiting someone, Harlot will not die
return

if harlot_target_id and harlot_target_id != harlot_id and Game.is_role_in_werewolf_party(self.players[harlot_target_id]):
# if Harlot visit Werewolf, Werewolf can't kill anyone
return

# check Cursed case
if type(self.players[killed]) is roles.Cursed: # pylint: disable=unidiomatic-typecheck
# pylint: disable=unidiomatic-typecheck
if type(self.players[killed]) is roles.Cursed and not self.players[killed].is_action_disabled_today():
await self.players[killed].set_active(True)
return

if killed == harlot_target_id and not self.players[harlot_target_id].is_protected():
self.night_pending_kill_list.append((harlot_id, const.DeadReason.SLEPT_OVER))

self.night_pending_kill_list.append((killed, const.DeadReason.HIDDEN))
await self.interface.send_action_text_to_channel(
"werewolf_kill_result_text", config.WEREWOLF_CHANNEL, target=f"<@{killed}>"
)
await self.do_werewolf_killed_effect(self.players[killed])

async def do_werewolf_killed_effect(self, killed_player):
if isinstance(killed_player, roles.Diseased):
if isinstance(killed_player, roles.Diseased) and not killed_player.is_action_disabled_today():
print("werewolf has been diseased")
self.is_werewolf_diseased = True
werewolf_list = self.get_werewolf_list()
Expand Down Expand Up @@ -1323,7 +1365,7 @@ async def do_player_action(self, cmd, author_id, *targets_id):
status=text_templates.get_word_in_language("alive" if is_alive_target_command else "dead")
)

if cmd in ("vote", "punish", "kill", "guard", "hunter", "seer", "reborn", "curse", "autopsy", "bite"):
if cmd in ("vote", "punish", "kill", "guard", "seer", "reborn", "curse", "hunter", "autopsy", "bite", "sleep"):
return await getattr(self, cmd)(author, targets[0])

if cmd == "ship":
Expand Down Expand Up @@ -1362,7 +1404,7 @@ async def __undo_player_nighttime_action(self, author_id, channel_name):
del self.wolf_kill_dict[author_id]
return text_templates.generate_text("undo_command_successful_text", player=f"<@{author_id}>")
# guard, hunter, seer, autospy, bite
if is_personal_channel and isinstance(player, (roles.Guard, roles.Hunter, roles.Seer, roles.ApprenticeSeer, roles.Pathologist, roles.Rat)) and player.get_target() is not None:
if is_personal_channel and isinstance(player, (roles.Guard, roles.Hunter, roles.Seer, roles.ApprenticeSeer, roles.Pathologist, roles.Rat, roles.Harlot)) and player.get_target() is not None:
player.set_target(None)
return text_templates.generate_text("undo_command_successful_text", player=f"<@{author_id}>")
# Undo curse, reborn just when Witch did any of them previously
Expand Down Expand Up @@ -1477,6 +1519,11 @@ async def hunter(self, author, target):
async def bite(self, author, target):
return author.register_target(target.player_id)

@command_verify_author(roles.Harlot)
@command_verify_phase(const.GamePhase.NIGHT)
async def sleep(self, author, target):
return author.register_target(target.player_id)

@staticmethod
def is_role_in_werewolf_party(player):
if type(player) is roles.Cursed: # pylint: disable=unidiomatic-typecheck
Expand Down
20 changes: 12 additions & 8 deletions game/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from enum import Enum, auto
from enum import Enum


class GamePhase(Enum):
Expand All @@ -22,11 +22,12 @@ def __repr__(self):


class DeadReason(StatusChangeReason):
HIDDEN = 0
TANNER_NO_VOTE = auto()
LYNCHED = auto()
HUNTED = auto()
COUPLE = auto()
HIDDEN = -1
TANNER_NO_VOTE = -2
LYNCHED = -3
HUNTED = -4
SLEPT_OVER = -5
COUPLE = -6

def is_couple_following(self):
return self == self.__class__.COUPLE
Expand All @@ -41,15 +42,18 @@ def get_template_label(self, game_phase):
if self == DeadReason.HUNTED:
return "hunter_killed_text"

if self == DeadReason.SLEPT_OVER:
return "harlot_died_by_slept_over_text"

if self == DeadReason.COUPLE:
return f"couple_died_on_{game_phase.name.lower()}_text"

return "killed_users_text"


class RebornReason(StatusChangeReason):
HIDDEN = 0
COUPLE = auto()
HIDDEN = 1
COUPLE = 2

def is_couple_following(self):
return self == self.__class__.COUPLE
Expand Down
3 changes: 2 additions & 1 deletion game/roles/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from game.roles.diseased import Diseased
from game.roles.rat import Rat
from game.roles.cursed import Cursed
from game.roles.harlot import Harlot
import utils


Expand All @@ -26,7 +27,7 @@

def get_all_roles():
return Villager, Werewolf, Seer, Guard, Lycan, Superwolf, Betrayer, Fox, Witch, Zombie, Cupid, Chief, Hunter,\
Tanner, Pathologist, Diseased, Rat, ApprenticeSeer, Cursed
Tanner, Pathologist, Diseased, Rat, ApprenticeSeer, Cursed, Harlot


def get_party_roles_list():
Expand Down
26 changes: 26 additions & 0 deletions game/roles/harlot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import text_templates
from game.roles.villager import Villager


class Harlot(Villager):
def __init__(self, interface, player_id, player_name):
super().__init__(interface, player_id, player_name)
self.yesterday_target = None

async def on_day(self):
self.yesterday_target = self.target
self.target = None

async def on_night_start(self, alive_embed_data, _):
if self.is_alive():
await self.interface.send_action_text_to_channel("harlot_before_voting_text", self.channel_name)
await self.interface.send_embed_to_channel(alive_embed_data, self.channel_name)

def is_yesterday_target(self, target_id):
return self.yesterday_target == target_id

def generate_invalid_target_text(self, target_id):
if self.is_yesterday_target(target_id):
return text_templates.generate_text("invalid_harlot_yesterday_target_text")

return ""
10 changes: 10 additions & 0 deletions json/command_info.json
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,16 @@
"exclusive_roles": ["Rat"],
"required_params": ["player_id"]
},
"sleep": {
"description": {
"vi": "Chọn một ai đó để đến ngủ cùng.",
"en": "Select a player to sleep over."
},
"type": "action",
"valid_channels": ["PERSONAL"],
"exclusive_roles": ["Harlot"],
"required_params": ["player_id"]
},
"zombie": {
"description": {
"vi": "Tự đội mồ sống dậy.",
Expand Down
6 changes: 5 additions & 1 deletion json/role_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
{"Werewolf": 2, "Seer": 1, "Guard": 1, "Hunter": 1, "Witch" : 1, "Cupid": 1, "Chief": 1},
{"Werewolf": 2, "Seer": 1, "Guard": 1, "ApprenticeSeer": 1, "Witch" : 1, "Cupid": 1, "Chief": 1},
{"Werewolf": 1, "Rat": 1, "Seer": 1, "Guard": 1, "Villager": 1, "Cupid": 1, "Pathologist": 1, "Chief": 1},
{"Werewolf": 1, "Seer": 1, "Guard": 1, "Villager": 1, "Harlot": 1, "Betrayer": 1, "Witch": 1, "Cupid": 1},
{"Werewolf": 2, "Seer": 1, "Guard": 1, "Chief": 1, "Witch": 1, "Cupid": 1, "Fox": 1, "Zombie": 1},
{"Werewolf": 2, "Seer": 1, "Guard": 1, "Chief": 1, "Witch": 1, "Cursed": 1, "Fox": 1, "Zombie": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Zombie": 1, "Fox": 1, "Lycan": 1, "Cupid": 1, "Witch": 1},
Expand All @@ -47,6 +48,7 @@
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Villager": 1, "Fox": 1, "Cupid": 1, "Chief": 1, "ApprenticeSeer": 1},
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Villager": 1, "Fox": 1, "Cursed": 1, "Chief": 1, "ApprenticeSeer": 1},
{"Werewolf": 1, "Rat": 1, "Seer": 1, "Guard": 1, "Villager": 1, "Fox": 1, "Tanner": 1, "Witch": 1, "Diseased": 1},
{"Werewolf": 1, "Cursed": 1, "Seer": 1, "Guard": 1, "Villager": 1, "Betrayer": 1, "Diseased": 1, "Harlot": 1, "Chief": 1},
{"Superwolf": 1, "Werewolf": 2, "Seer": 1, "Guard": 1, "Villager": 1, "Tanner": 1, "Diseased": 1, "Fox": 1, "Cupid": 1, "Pathologist": 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},
Expand All @@ -66,6 +68,7 @@
{"Superwolf": 1, "Werewolf": 1, "Seer": 1, "Guard": 1, "Fox": 1, "Witch": 1, "Villager": 1, "Lycan": 2, "Cupid": 1, "Betrayer": 1, "Zombie": 1, "Hunter": 1},
{"Superwolf": 1, "Werewolf": 2, "Seer": 1, "Guard": 1, "Villager": 1, "Betrayer": 1, "Pathologist": 1, "Diseased": 1, "Cupid": 1, "Lycan": 1, "Witch": 1},
{"Superwolf": 1, "Werewolf": 2, "Seer": 1, "Guard": 1, "Villager": 1, "Betrayer": 1, "Pathologist": 1, "Diseased": 1, "Cursed": 1, "Lycan": 1, "Witch": 1},
{"Superwolf": 1, "Werewolf": 1, "Rat": 1, "Seer": 1, "Guard": 1, "Villager": 1, "Cursed": 1, "Tanner": 1, "Harlot": 1, "Lycan": 1, "Fox": 1, "Zombie": 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},
{"Werewolf": 3, "Seer": 1, "Guard": 1, "Witch": 1, "Villager": 4, "Lycan": 1, "Cupid": 1, "ApprenticeSeer": 1, "Chief": 1},
Expand All @@ -74,5 +77,6 @@
{"Superwolf": 1, "Werewolf": 1, "Rat": 1, "Seer": 1, "Guard": 1, "Villager": 1, "Betrayer": 1, "Cupid": 1, "Tanner": 1, "Witch": 1, "Fox": 1, "Chief": 1, "Hunter": 1},
{"Superwolf": 1, "Werewolf": 1, "Rat": 1, "Seer": 1, "Guard": 1, "Villager": 1, "Betrayer": 1, "Cursed": 1, "Tanner": 1, "Witch": 1, "Fox": 1, "Chief": 1, "Hunter": 1},
{"Superwolf": 1, "Werewolf": 2, "Seer": 1, "Guard": 1, "Witch": 1, "Villager": 5, "Lycan": 1, "Fox": 1, "Cupid": 1},
{"Superwolf": 1, "Werewolf": 2, "Seer": 1, "Guard": 1, "Witch": 1, "Villager": 5, "Lycan": 1, "Fox": 1, "Cursed": 1}
{"Superwolf": 1, "Werewolf": 2, "Seer": 1, "Guard": 1, "Witch": 1, "Villager": 5, "Lycan": 1, "Fox": 1, "Cursed": 1},
{"Superwolf": 1, "Werewolf": 1, "Rat": 1, "Seer": 1, "Guard": 1, "Villager": 1, "Betrayer": 1, "Cursed": 1, "Cupid": 1, "Witch": 1, "Fox": 1, "Hunter": 1, "Harlot": 1, "Pathologist": 1}
]
1 change: 1 addition & 0 deletions json/role_generator_info.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"Betrayer": -2,
"Cupid": -3,
"Cursed": -3,
"Harlot": -3,
"Rat": -4,
"Werewolf": -5,
"Superwolf": -6
Expand Down
10 changes: 10 additions & 0 deletions json/role_info.json
Original file line number Diff line number Diff line change
Expand Up @@ -188,5 +188,15 @@
"party": 1,
"daytime_command": "vote",
"nighttime_commands": ["kill"]
},
"Harlot": {
"name_vi": "Kỹ nữ",
"description": {
"vi": "Kỹ nữ được lựa chọn một người để đến ngủ thăm vào buổi đêm.\n\nNếu Kỹ nữ đến thăm nạn nhân của Sói, Kỹ nữ sẽ chết theo kẻ đó.\nNếu Kỹ nữ đến thăm Sói, Sói sẽ không cắn được ai và Kỹ nữ sẽ chết.\nNếu Kỹ nữ bị Sói nhắm trúng mà Kỹ nữ đi thăm người khác, Kỹ nữ sẽ không bị sói cắn.",
"en": "Harlot can select a person to visit each night.\n\nIf Harlot visits Werewolf's victim, Harlot will die with them.\nIf Harlot visits a Werewolf, the Werewolves can't bite anyone and Harlot will die.\nIf Harlot is Werewolf's victim but Harlot was visiting other player, Harlot will not be killed by Werewolf."
},
"party": 1,
"daytime_command": "vote",
"nighttime_commands": ["sleep"]
}
}
48 changes: 48 additions & 0 deletions json/text_template.json
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,54 @@
"en": ["👀 This night, AprrenticeSeer wants to see {target}."]
}
},
"harlot_before_voting_text": {
"params": [],
"template": {
"vi": [
"🛏️ Kỹ nữ muốn ghé đến nhà ai đêm nay, hãy nhập `{bot_prefix}sleep ID` để qua đêm với người đó.",
"💡 Ví dụ: `{bot_prefix}sleep 2`"
],
"en": [
"🛏️ The harlot wants to visit someone's home tonight, enter `{bot_prefix}sleep ID` to sleep over in that person's house.",
"💡 Example: `{bot_prefix}sleep 2`"
]
}
},
"harlot_after_voting_text": {
"params": ["target"],
"template": {
"vi": ["🛏️ Đêm nay, Kỹ nữ muốn đến ngủ lại nhà {target}."],
"en": ["🛏️ This night, Harlot wants to sleep over in {target}'s house."]
}
},
"harlot_result_text": {
"params": ["target"],
"template": {
"vi": ["🛌🏻 Đã chốt đơn với {target} ;)"],
"en": ["🛌🏻 You have picked {target} ;)"]
}
},
"harlot_sleep_alone_result_text": {
"params": [],
"template": {
"vi": ["🛌🏻 Đêm nay bạn ngủ một mình :("],
"en": ["🛌🏻 You sleep alone tonight :("]
}
},
"invalid_harlot_yesterday_target_text": {
"params": [],
"template": {
"vi": ["Hôm qua bạn đã ghé qua ngủ nhà người này. Hãy đổi mục tiêu khác hôm nay!"],
"en": ["You slept over with this person yesterday. Let's change the target today!"]
}
},
"harlot_died_by_slept_over_text": {
"params": ["user"],
"template": {
"vi": ["Hôm qua {user} đã đi ngủ lang đâu đó cả đêm và không thấy về nữa."],
"en": ["Yesterday {user} went sleeping over with somebody all night and never came back."]
}
},
"zombie_before_voting_text": {
"params": [],
"template": {
Expand Down
Loading

0 comments on commit b358aea

Please sign in to comment.