hangman: add --hard and --extended flags
--hard (-h): words 9-15 letters instead of 5-8 --extended (-e): 10 wrong guesses with full body (feet + ears stages) Flags are combinable: !hangman --hard --extended Board header shows mode emoji (🔥 hard, 💀 extended, 💀🔥 both). Wrong counter shows X/10 in extended mode. All guess logic reads max_wrong from game dict instead of hardcoded 6. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+66
-30
@@ -1351,6 +1351,31 @@ _HANGMAN_STAGES = [
|
|||||||
"```\n +---+\n | |\n O |\n /|\\ |\n / \\ |\n |\n=========```",
|
"```\n +---+\n | |\n O |\n /|\\ |\n / \\ |\n |\n=========```",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
_HANGMAN_STAGES_EXTENDED = [
|
||||||
|
# 0 wrong
|
||||||
|
"```\n +---+\n | |\n |\n |\n |\n |\n=========```",
|
||||||
|
# 1 wrong - head
|
||||||
|
"```\n +---+\n | |\n O |\n |\n |\n |\n=========```",
|
||||||
|
# 2 wrong - body
|
||||||
|
"```\n +---+\n | |\n O |\n | |\n |\n |\n=========```",
|
||||||
|
# 3 wrong - left arm
|
||||||
|
"```\n +---+\n | |\n O |\n /| |\n |\n |\n=========```",
|
||||||
|
# 4 wrong - right arm
|
||||||
|
"```\n +---+\n | |\n O |\n /|\\ |\n |\n |\n=========```",
|
||||||
|
# 5 wrong - left leg
|
||||||
|
"```\n +---+\n | |\n O |\n /|\\ |\n / |\n |\n=========```",
|
||||||
|
# 6 wrong - right leg
|
||||||
|
"```\n +---+\n | |\n O |\n /|\\ |\n / \\ |\n |\n=========```",
|
||||||
|
# 7 wrong - left foot
|
||||||
|
"```\n +---+\n | |\n O |\n /|\\ |\n / \\ |\n/ |\n=========```",
|
||||||
|
# 8 wrong - right foot
|
||||||
|
"```\n +---+\n | |\n O |\n /|\\ |\n / \\ |\n/ \\ |\n=========```",
|
||||||
|
# 9 wrong - left ear
|
||||||
|
"```\n +---+\n | |\n \\O |\n /|\\ |\n / \\ |\n/ \\ |\n=========```",
|
||||||
|
# 10 wrong (dead) - both ears
|
||||||
|
"```\n +---+\n | |\n \\O/ |\n /|\\ |\n / \\ |\n/ \\ |\n=========```",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _hangman_display(game: dict) -> str:
|
def _hangman_display(game: dict) -> str:
|
||||||
word = game["word"]
|
word = game["word"]
|
||||||
@@ -1362,23 +1387,32 @@ def _hangman_board_html(game: dict, status_line: str = "") -> tuple[str, str]:
|
|||||||
"""Return (plain, html) for the current hangman board state."""
|
"""Return (plain, html) for the current hangman board state."""
|
||||||
word = game["word"]
|
word = game["word"]
|
||||||
wrong_count = game["wrong_count"]
|
wrong_count = game["wrong_count"]
|
||||||
|
max_wrong = game.get("max_wrong", 6)
|
||||||
|
stages = _HANGMAN_STAGES_EXTENDED if game.get("extended") else _HANGMAN_STAGES
|
||||||
display = _hangman_display(game)
|
display = _hangman_display(game)
|
||||||
wrong_letters = sorted(ch for ch in game["guessed_letters"] if ch not in word)
|
wrong_letters = sorted(ch for ch in game["guessed_letters"] if ch not in word)
|
||||||
stage_art = _HANGMAN_STAGES[wrong_count].replace("```", "")
|
stage_art = stages[min(wrong_count, len(stages) - 1)].replace("```", "")
|
||||||
|
mode_tag = ""
|
||||||
|
if game.get("hard") and game.get("extended"):
|
||||||
|
mode_tag = " 💀🔥"
|
||||||
|
elif game.get("hard"):
|
||||||
|
mode_tag = " 🔥"
|
||||||
|
elif game.get("extended"):
|
||||||
|
mode_tag = " 💀"
|
||||||
|
|
||||||
plain = (
|
plain = (
|
||||||
f"🎯 Hangman!\n{stage_art}\n"
|
f"🎯 Hangman{mode_tag}!\n{stage_art}\n"
|
||||||
f"Word: {display} ({len(word)} letters)\n"
|
f"Word: {display} ({len(word)} letters)\n"
|
||||||
f"Hint: {game['hint']}\n"
|
f"Hint: {game['hint']}\n"
|
||||||
f"Wrong ({wrong_count}/6): {', '.join(wrong_letters) or 'none'}"
|
f"Wrong ({wrong_count}/{max_wrong}): {', '.join(wrong_letters) or 'none'}"
|
||||||
+ (f"\n{status_line}" if status_line else "")
|
+ (f"\n{status_line}" if status_line else "")
|
||||||
)
|
)
|
||||||
html = (
|
html = (
|
||||||
f'<font color="#f59e0b"><strong>🎯 Hangman!</strong></font><br>'
|
f'<font color="#f59e0b"><strong>🎯 Hangman{mode_tag}!</strong></font><br>'
|
||||||
f'<pre>{stage_art}</pre>'
|
f'<pre>{stage_art}</pre>'
|
||||||
f'<strong>Word:</strong> <code>{display}</code> ({len(word)} letters)<br>'
|
f'<strong>Word:</strong> <code>{display}</code> ({len(word)} letters)<br>'
|
||||||
f'<strong>Hint:</strong> {game["hint"]}<br>'
|
f'<strong>Hint:</strong> {game["hint"]}<br>'
|
||||||
f'Wrong ({wrong_count}/6): {", ".join(wrong_letters) or "none"}'
|
f'Wrong ({wrong_count}/{max_wrong}): {", ".join(wrong_letters) or "none"}'
|
||||||
+ (f'<br><em>{status_line}</em>' if status_line else "")
|
+ (f'<br><em>{status_line}</em>' if status_line else "")
|
||||||
)
|
)
|
||||||
return plain, html
|
return plain, html
|
||||||
@@ -1406,7 +1440,7 @@ def _save_hangman_cache(words: list[str]) -> None:
|
|||||||
_hangman_recent: list[str] = _load_hangman_cache()
|
_hangman_recent: list[str] = _load_hangman_cache()
|
||||||
|
|
||||||
|
|
||||||
async def _generate_hangman_word() -> dict | None:
|
async def _generate_hangman_word(min_len: int = 5, max_len: int = 8) -> dict | None:
|
||||||
avoid_clause = (
|
avoid_clause = (
|
||||||
" Do NOT use any of these recently used words: "
|
" Do NOT use any of these recently used words: "
|
||||||
+ ", ".join(f'"{w}"' for w in _hangman_recent[-20:])
|
+ ", ".join(f'"{w}"' for w in _hangman_recent[-20:])
|
||||||
@@ -1416,7 +1450,10 @@ async def _generate_hangman_word() -> dict | None:
|
|||||||
"You are a hangman game generator. Always respond with ONLY a JSON object — no markdown, no explanation. "
|
"You are a hangman game generator. Always respond with ONLY a JSON object — no markdown, no explanation. "
|
||||||
'Format: {"word": "example", "hint": "short category or hint"}'
|
'Format: {"word": "example", "hint": "short category or hint"}'
|
||||||
)
|
)
|
||||||
user_msg = f"Pick a common English word between 5 and 8 letters (lowercase letters only, no hyphens or spaces) and give a short hint.{avoid_clause}"
|
user_msg = (
|
||||||
|
f"Pick a common English word between {min_len} and {max_len} letters "
|
||||||
|
f"(lowercase letters only, no hyphens or spaces) and give a short hint.{avoid_clause}"
|
||||||
|
)
|
||||||
for attempt in range(2):
|
for attempt in range(2):
|
||||||
try:
|
try:
|
||||||
timeout = aiohttp.ClientTimeout(total=60)
|
timeout = aiohttp.ClientTimeout(total=60)
|
||||||
@@ -1445,7 +1482,7 @@ async def _generate_hangman_word() -> dict | None:
|
|||||||
parsed = {}
|
parsed = {}
|
||||||
word = parsed.get("word", "").lower().strip()
|
word = parsed.get("word", "").lower().strip()
|
||||||
hint = parsed.get("hint", "").strip()
|
hint = parsed.get("hint", "").strip()
|
||||||
if word.isalpha() and 5 <= len(word) <= 8 and hint:
|
if word.isalpha() and min_len <= len(word) <= max_len and hint:
|
||||||
_hangman_recent.append(word)
|
_hangman_recent.append(word)
|
||||||
if len(_hangman_recent) > _HANGMAN_RECENT_MAX:
|
if len(_hangman_recent) > _HANGMAN_RECENT_MAX:
|
||||||
_hangman_recent.pop(0)
|
_hangman_recent.pop(0)
|
||||||
@@ -1457,46 +1494,43 @@ async def _generate_hangman_word() -> dict | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@command("hangman", "Play hangman! AI picks a word, guess letters with !guess")
|
@command("hangman", "Play hangman — flags: --hard (long words), --extended (10 guesses + more body parts)")
|
||||||
async def cmd_hangman(client: AsyncClient, room_id: str, sender: str, args: str):
|
async def cmd_hangman(client: AsyncClient, room_id: str, sender: str, args: str):
|
||||||
if room_id in _HANGMAN_GAMES:
|
if room_id in _HANGMAN_GAMES:
|
||||||
game = _HANGMAN_GAMES[room_id]
|
plain, html = _hangman_board_html(_HANGMAN_GAMES[room_id], "Use !guess <letter> or !guess <word>")
|
||||||
display = _hangman_display(game)
|
await send_html(client, room_id, plain, html)
|
||||||
wrong = game["wrong_count"]
|
|
||||||
guessed = sorted(game["guessed_letters"])
|
|
||||||
wrong_letters = [ch for ch in guessed if ch not in game["word"]]
|
|
||||||
plain = (
|
|
||||||
f"Hangman already in progress!\n"
|
|
||||||
f"{_HANGMAN_STAGES[wrong]}\n"
|
|
||||||
f"Word: {display}\n"
|
|
||||||
f"Hint: {game['hint']}\n"
|
|
||||||
f"Wrong guesses ({wrong}/6): {', '.join(wrong_letters) or 'none'}\n"
|
|
||||||
f"Use !guess <letter> or !guess <word>"
|
|
||||||
)
|
|
||||||
await send_text(client, room_id, plain)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Parse flags
|
||||||
|
flags = args.lower().split()
|
||||||
|
hard = "--hard" in flags or "-h" in flags
|
||||||
|
extended = "--extended" in flags or "-e" in flags
|
||||||
|
max_wrong = 10 if extended else 6
|
||||||
|
min_len, max_len = (9, 15) if hard else (5, 8)
|
||||||
|
|
||||||
await send_text(client, room_id, "🎯 Picking a word...")
|
await send_text(client, room_id, "🎯 Picking a word...")
|
||||||
|
|
||||||
word_data = await _generate_hangman_word()
|
word_data = await _generate_hangman_word(min_len=min_len, max_len=max_len)
|
||||||
if word_data is None:
|
if word_data is None:
|
||||||
await send_text(client, room_id, "Failed to generate a word. Try again later.")
|
await send_text(client, room_id, "Failed to generate a word. Try again later.")
|
||||||
return
|
return
|
||||||
|
|
||||||
word = word_data["word"]
|
word = word_data["word"]
|
||||||
hint = word_data["hint"]
|
hint = word_data["hint"]
|
||||||
display = " ".join("_" for _ in word)
|
|
||||||
|
|
||||||
game = {
|
game = {
|
||||||
"word": word,
|
"word": word,
|
||||||
"hint": hint,
|
"hint": hint,
|
||||||
"guessed_letters": set(),
|
"guessed_letters": set(),
|
||||||
"wrong_count": 0,
|
"wrong_count": 0,
|
||||||
|
"max_wrong": max_wrong,
|
||||||
|
"hard": hard,
|
||||||
|
"extended": extended,
|
||||||
"board_event_id": None,
|
"board_event_id": None,
|
||||||
}
|
}
|
||||||
_HANGMAN_GAMES[room_id] = game
|
_HANGMAN_GAMES[room_id] = game
|
||||||
|
|
||||||
plain, html = _hangman_board_html(game, "Guess with !guess <letter/word> — max 6 wrong guesses")
|
plain, html = _hangman_board_html(game, f"Guess with !guess <letter/word> — max {max_wrong} wrong guesses")
|
||||||
resp = await send_html(client, room_id, plain, html)
|
resp = await send_html(client, room_id, plain, html)
|
||||||
if hasattr(resp, "event_id"):
|
if hasattr(resp, "event_id"):
|
||||||
game["board_event_id"] = resp.event_id
|
game["board_event_id"] = resp.event_id
|
||||||
@@ -1527,6 +1561,8 @@ async def cmd_guess(client: AsyncClient, room_id: str, sender: str, args: str):
|
|||||||
else:
|
else:
|
||||||
await send_html(client, room_id, p, h)
|
await send_html(client, room_id, p, h)
|
||||||
|
|
||||||
|
max_wrong = game.get("max_wrong", 6)
|
||||||
|
|
||||||
# Full word guess
|
# Full word guess
|
||||||
if len(guess) > 1:
|
if len(guess) > 1:
|
||||||
winner = sender.split(":")[0].lstrip("@")
|
winner = sender.split(":")[0].lstrip("@")
|
||||||
@@ -1539,11 +1575,11 @@ async def cmd_guess(client: AsyncClient, room_id: str, sender: str, args: str):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
game["wrong_count"] += 1
|
game["wrong_count"] += 1
|
||||||
if game["wrong_count"] >= 6:
|
if game["wrong_count"] >= max_wrong:
|
||||||
del _HANGMAN_GAMES[room_id]
|
del _HANGMAN_GAMES[room_id]
|
||||||
await _update_board(f"💀 Wrong! Game over — the word was: {word.upper()}")
|
await _update_board(f"💀 Wrong! Game over — the word was: {word.upper()}")
|
||||||
else:
|
else:
|
||||||
remaining = 6 - game["wrong_count"]
|
remaining = max_wrong - game["wrong_count"]
|
||||||
await _update_board(f"❌ '{guess.upper()}' is wrong! {remaining} guesses remaining.")
|
await _update_board(f"❌ '{guess.upper()}' is wrong! {remaining} guesses remaining.")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1566,11 +1602,11 @@ async def cmd_guess(client: AsyncClient, room_id: str, sender: str, args: str):
|
|||||||
game["wrong_count"] += 1
|
game["wrong_count"] += 1
|
||||||
wrong_count = game["wrong_count"]
|
wrong_count = game["wrong_count"]
|
||||||
|
|
||||||
if wrong_count >= 6:
|
if wrong_count >= max_wrong:
|
||||||
del _HANGMAN_GAMES[room_id]
|
del _HANGMAN_GAMES[room_id]
|
||||||
await _update_board(f"💀 Game over! The word was: {word.upper()}")
|
await _update_board(f"💀 Game over! The word was: {word.upper()}")
|
||||||
else:
|
else:
|
||||||
remaining = 6 - wrong_count
|
remaining = max_wrong - wrong_count
|
||||||
await _update_board(f"❌ '{letter.upper()}' not in the word — {remaining} guesses left.")
|
await _update_board(f"❌ '{letter.upper()}' not in the word — {remaining} guesses left.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user