Improve stamps end screen (#101)

* properly handle players exiting cardjitsu water

* make end screens only show stamps from current session

* add card-jitsu end game screen

* fix oversight with how water players get removed

* fix card jitsu fire stamp awards

* change session stamps to use redis
This commit is contained in:
nhaar 2024-10-12 01:16:52 -03:00 committed by GitHub
parent f6f9f39c22
commit e23a7ecf2e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 234 additions and 83 deletions

View File

@ -78,30 +78,34 @@ async def handle_get_game_over(p, score: int):
if p.room.id == 996:
return
if p.room.game and not p.waddle and not p.table:
# card-jitsus except snow have special handling
card_jitsu_rooms = [995, 998, 997]
is_card_jitsu = p.room.id in card_jitsu_rooms
# Waddle minigames don't normally need the end screen
if p.waddle and not is_card_jitsu:
return
if p.room.game and not p.table:
coins_earned = determine_coins_earned(p, score)
if await determine_coins_overdose(p, coins_earned):
return await cheat_ban(p, p.id, comment='Coins overdose')
collected_stamps_string, total_collected_stamps, total_game_stamps, total_stamps = '', 0, 0, 0
if not is_card_jitsu:
if await determine_coins_overdose(p, coins_earned):
return await cheat_ban(p, p.id, comment="Coins overdose")
stamp_info = "", 0, 0, 0
if p.room.stamp_group:
game_stamps = [stamp for stamp in p.server.stamps.values() if stamp.group_id == p.room.stamp_group]
collected_stamps = [stamp for stamp in game_stamps if stamp.id in p.stamps]
total_stamps = len([stamp for stamp in p.stamps.values() if p.server.stamps[stamp.stamp_id].group_id])
total_collected_stamps = len(collected_stamps)
total_game_stamps = len(game_stamps)
collected_stamps_string = '|'.join(str(stamp.id) for stamp in collected_stamps)
if total_collected_stamps == total_game_stamps:
stamp_info = await p.get_game_end_stamps_info(True)
# has all stamps in game
if stamp_info[1] == stamp_info[2]:
coins_earned *= 2
await p.update(coins=min(p.coins + coins_earned, p.server.config.max_coins)).apply()
await p.send_xt('zo', p.coins,
collected_stamps_string,
total_collected_stamps,
total_game_stamps,
total_stamps)
if not is_card_jitsu:
await p.update(
coins=min(p.coins + coins_earned, p.server.config.max_coins)
).apply()
await p.send_xt("zo", p.coins, *stamp_info)
@handlers.handler(XTPacket('ggd', ext='z'), client=ClientType.Vanilla)
@ -146,3 +150,10 @@ async def handle_game_complete(p, medals: int):
medals = min(6, medals)
await p.update(career_medals=p.career_medals + medals,
agent_medals=p.agent_medals + medals).apply()
@handlers.disconnected
@handlers.player_attribute(joined_world=True)
async def clear_stamp_sessions(p):
"""When disconnected, clear stamps in case any were obtained and not properly handled"""
await p.clear_stamps_session()

View File

@ -1,6 +1,7 @@
import itertools
import math
import random
import enum
from collections import Counter
from dataclasses import dataclass
from typing import Dict, List, Union
@ -10,6 +11,19 @@ from houdini.data.ninja import Card
from houdini.handlers import XTPacket
from houdini.penguin import Penguin
class CardStamp(enum.IntEnum):
"""ID of Card-Jitsu stamps"""
GRASSHOPPER = 230
ELEMENTAL_WIN = 242
FINE_STUDENT = 232
FLAWLESS_VICTORY = 238
ONE_ELEMENT = 244
TRUE_NINJA = 234
MATCH_MASTER = 240
NINJA_MASTER = 236
SENSEI_CARD = 246
FULL_DOJO = 248
@dataclass
class Played:
@ -44,7 +58,12 @@ class CardJitsuLogic(IWaddle):
ItemAwards = [4025, 4026, 4027, 4028, 4029, 4030, 4031, 4032, 4033, 104]
PostcardAwards = {0: 177, 4: 178, 8: 179}
StampAwards = {0: 230, 4: 232, 8: 234, 9: 236}
StampAwards = {
0: CardStamp.GRASSHOPPER,
4: CardStamp.FINE_STUDENT,
8: CardStamp.TRUE_NINJA,
9: CardStamp.NINJA_MASTER,
}
StampGroupId = 38
def __init__(self, waddle):
@ -235,7 +254,7 @@ async def ninja_rank_up(p, ranks=1):
if rank in CardJitsuLogic.PostcardAwards:
await p.add_inbox(p.server.postcards[CardJitsuLogic.PostcardAwards[rank]])
if rank in CardJitsuLogic.StampAwards:
await p.add_stamp(p.server.stamps[CardJitsuLogic.StampAwards[rank]])
await p.add_card_jitsu_stamp(CardJitsuLogic.StampAwards[rank])
await p.update(ninja_rank=p.ninja_rank + ranks).apply()
return True
@ -266,20 +285,10 @@ async def ninja_progress(p, won=False):
await ninja_rank_up(p)
await p.send_xt('cza', p.ninja_rank)
async def ninja_stamps_earned(p):
game_stamps = [stamp for stamp in p.server.stamps.values() if stamp.group_id == p.room.stamp_group]
collected_stamps = [stamp for stamp in game_stamps if stamp.id in p.stamps]
total_collected_stamps = len(collected_stamps)
total_game_stamps = len(game_stamps)
collected_stamps_string = '|'.join(str(stamp.id) for stamp in collected_stamps)
await p.send_xt('cjsi', collected_stamps_string, total_collected_stamps, total_game_stamps)
async def ninja_win(winner, loser):
await ninja_progress(winner.penguin, won=True)
await ninja_progress(loser.penguin, won=False)
await ninja_stamps_earned(winner.penguin)
await ninja_stamps_earned(loser.penguin)
await winner.penguin.waddle.remove_penguin(winner.penguin)
await loser.penguin.waddle.remove_penguin(loser.penguin)
@ -304,6 +313,8 @@ async def handle_update_game(p):
@handlers.handler(XTPacket('lz', ext='z'))
@handlers.waddle(CardJitsuLogic, CardJitsuMatLogic, SenseiLogic)
async def handle_leave_game(p):
# sending stamp info fixes the game taking a bit to close when quitting
await p.send_card_jitsu_stamp_info()
seat_id = p.waddle.get_seat_id(p)
await p.waddle.send_xt('cz', p.safe_name, f=lambda penguin: penguin is not p)
await p.waddle.send_xt('lz', seat_id, f=lambda penguin: penguin is not p)
@ -357,9 +368,8 @@ async def handle_send_pick(p, action: str, card_id: int):
winner_seat_id = p.waddle.get_round_winner()
if me.chosen.card.id == 256 or opponent.chosen.card.id == 256:
stamp = p.server.stamps[246]
await me.penguin.add_stamp(stamp, notify=True)
await opponent.penguin.add_stamp(stamp, notify=True)
await me.penguin.add_card_jitsu_stamp(CardStamp.SENSEI_CARD)
await opponent.penguin.add_card_jitsu_stamp(CardStamp.SENSEI_CARD)
if me.chosen.card.power_id and me.chosen.card.power_id in CardJitsuLogic.OnPlayed:
await p.waddle.send_xt('zm', 'power', seat_id, opponent_seat_id, me.chosen.card.power_id)
@ -383,19 +393,18 @@ async def handle_send_pick(p, action: str, card_id: int):
if winning_cards:
await p.waddle.send_xt('czo', 0, winner_seat_id, *(card.id for card in winning_cards))
stamp = p.server.stamps[[244, 242][win_method]]
await winner.penguin.add_stamp(stamp, notify=True)
stamp = [CardStamp.ONE_ELEMENT, CardStamp.ELEMENTAL_WIN][win_method]
await winner.penguin.add_card_jitsu_stamp(stamp)
if all(not cards for cards in loser.bank.values()):
stamp = p.server.stamps[238]
await winner.penguin.add_stamp(stamp, notify=True)
await winner.penguin.add_card_jitsu_stamp(
CardStamp.FLAWLESS_VICTORY
)
if sum(1 for cards in winner.bank.values() for _ in cards) >= 9:
stamp = p.server.stamps[248]
await winner.penguin.add_stamp(stamp, notify=True)
await winner.penguin.add_card_jitsu_stamp(CardStamp.FULL_DOJO)
await winner.penguin.update(ninja_matches_won=winner.penguin.ninja_matches_won+1).apply()
if winner.penguin.ninja_matches_won == 25:
stamp = p.server.stamps[240]
await winner.penguin.add_stamp(stamp, notify=True)
await winner.penguin.add_card_jitsu_stamp(CardStamp.MATCH_MASTER)
await ninja_win(winner, loser)
else:
@ -493,8 +502,7 @@ async def handle_send_sensei_pick(p, action: str, card_id: int):
winner_seat_id = p.waddle.get_round_winner()
if me.chosen.card.id == 256 or sensei.chosen.card.id == 256:
stamp = p.server.stamps[246]
await p.add_stamp(stamp, notify=True)
await me.penguin.add_card_jitsu_stamp(CardStamp.SENSEI_CARD)
if me.chosen.card.power_id and me.chosen.card.power_id in CardJitsuLogic.OnPlayed:
await p.send_xt('zm', 'power', 1, 0, me.chosen.card.power_id)
@ -518,21 +526,17 @@ async def handle_send_sensei_pick(p, action: str, card_id: int):
await p.waddle.send_xt('czo', 0, winner_seat_id, *(card.id for card in winning_cards))
if winner == me:
stamp = p.server.stamps[[244, 242][win_method]]
await p.add_stamp(stamp, notify=True)
stamp = [CardStamp.ONE_ELEMENT, CardStamp.ELEMENTAL_WIN][win_method]
await me.penguin.add_card_jitsu_stamp(stamp)
if all(not cards for cards in sensei.bank.values()):
stamp = p.server.stamps[238]
await p.add_stamp(stamp, notify=True)
await me.penguin.add_card_jitsu_stamp(CardStamp.FLAWLESS_VICTORY)
if sum(1 for cards in me.bank.values() for _ in cards) >= 9:
stamp = p.server.stamps[248]
await p.add_stamp(stamp, notify=True)
await me.penguin.add_card_jitsu_stamp(CardStamp.FULL_DOJO)
await p.update(ninja_matches_won=p.ninja_matches_won + 1).apply()
if p.ninja_matches_won == 25:
stamp = p.server.stamps[240]
await p.add_stamp(stamp, notify=True)
await me.penguin.add_card_jitsu_stamp(CardStamp.MATCH_MASTER)
await ninja_stamps_earned(p)
can_rank_up = await ninja_rank_up(p)
if can_rank_up:
await p.send_xt('cza', p.ninja_rank)
@ -548,7 +552,6 @@ async def handle_send_sensei_pick(p, action: str, card_id: int):
if can_rank_up:
await p.send_xt('cza', p.ninja_rank)
await p.waddle.send_xt('czo', 0, winner_seat_id)
await ninja_stamps_earned(p)
await p.send_xt('zm', 'judge', winner_seat_id)
me.chosen = None

View File

@ -2,6 +2,7 @@ import asyncio
import itertools
import math
import random
import enum
from collections import Counter
from dataclasses import dataclass, field
from typing import List, Union
@ -9,9 +10,18 @@ from typing import List, Union
from houdini import IWaddle, handlers
from houdini.data.ninja import Card
from houdini.handlers import XTPacket
from houdini.handlers.games.ninja.card import ninja_stamps_earned
from houdini.penguin import Penguin
class FireStamp(enum.IntEnum):
"""IDs of Card-Jitsu Fire stamps"""
WARM_UP = 252
SCORE_FIRE = 254
FIRE_MIDWAY = 256
STRONG_DEFENCE = 260
FIRE_SUIT = 262
FIRE_NINJA = 264
MAX_ENERGY = 266
FIRE_EXPERT = 268
@dataclass
class FireNinja:
@ -37,7 +47,7 @@ class CardJitsuFireLogic(IWaddle):
AutoBattleTimeout = 22
ItemAwards = [6025, 4120, 2013, 1086, 3032]
StampAwards = {2: 256, 4: 262}
StampAwards = {1: FireStamp.FIRE_MIDWAY, 3: FireStamp.FIRE_SUIT}
def __init__(self, waddle):
super().__init__(waddle)
@ -524,7 +534,7 @@ async def fire_ninja_rank_up(p, ranks=1):
for rank in range(p.fire_ninja_rank, p.fire_ninja_rank+ranks):
await p.add_inventory(p.server.items[CardJitsuFireLogic.ItemAwards[rank]], notify=False)
if rank in CardJitsuFireLogic.StampAwards:
await p.add_stamp(p.server.stamps[CardJitsuFireLogic.StampAwards[rank]])
await p.add_card_jitsu_stamp(CardJitsuFireLogic.StampAwards[rank])
await p.update(
fire_ninja_rank=p.fire_ninja_rank + ranks
).apply()
@ -567,18 +577,18 @@ async def end_game_stamps(ninja, finish_position):
if finish_position == 1:
await ninja.penguin.update(fire_matches_won=ninja.penguin.fire_matches_won + 1).apply()
if ninja.penguin.fire_matches_won >= 10:
await ninja.penguin.add_stamp(ninja.penguin.server.stamps[252])
await ninja.penguin.add_card_jitsu_stamp(FireStamp.WARM_UP)
if ninja.penguin.fire_matches_won >= 50:
await ninja.penguin.add_stamp(ninja.penguin.server.stamps[268])
await ninja.penguin.add_card_jitsu_stamp(FireStamp.FIRE_EXPERT)
if ninja.energy >= 6:
await ninja.penguin.add_stamp(ninja.penguin.server.stamps[260])
await ninja.penguin.add_card_jitsu_stamp(FireStamp.STRONG_DEFENCE)
if type(ninja.penguin.waddle) == FireSenseiLogic:
await ninja.penguin.add_stamp(ninja.penguin.server.stamps[264])
await ninja.penguin.add_card_jitsu_stamp(FireStamp.FIRE_NINJA)
if ninja.energy_won >= 1:
await ninja.penguin.add_stamp(ninja.penguin.server.stamps[254])
await ninja.penguin.add_card_jitsu_stamp(FireStamp.SCORE_FIRE)
if ninja.energy_won >= 3:
await ninja.penguin.add_stamp(ninja.penguin.server.stamps[266])
await ninja.penguin.add_card_jitsu_stamp(FireStamp.MAX_ENERGY)
@handlers.handler(XTPacket('gz', ext='z'))
@ -665,5 +675,5 @@ async def handle_info_ready_sync(p):
@handlers.handler(XTPacket('lz', ext='z'))
@handlers.player_in_room(CardJitsuFireLogic.room_id)
async def handle_leave_game(p):
await ninja_stamps_earned(p)
async def handle_leave_game(p: Penguin):
await p.send_card_jitsu_stamp_info()

View File

@ -12,6 +12,17 @@ from houdini.handlers import XTPacket
from houdini.penguin import Penguin
from houdini.data.ninja import Card
class WaterStamp(enum.IntEnum):
"IDs of Card-Jitsu Water stamps"
GONG = 270
WATERY_FALL = 274
WATER_EXPERT = 276
WATER_MIDWAY = 278
WATER_SUIT = 282
WATER_NINJA = 284
TWO_CLOSE = 286
SKIPPING_STONES = 288
@dataclass
class WaterCard:
@ -252,6 +263,13 @@ class WaterPlayer:
cleared: int = 0
"""Number of stones cleared in the match"""
left: bool = False
"""
Whether the player has left the game
Penguins remain in the board even if the player leaves
"""
def get_card(self, hand_id: int) -> WaterCard:
"""Get the card given its hand ID"""
return next((card for card in self.hand.cards if card.hand_id == hand_id), None)
@ -407,7 +425,11 @@ class CardJitsuWaterLogic(IWaddle):
ITEM_AWARDS = [6026, 4121, 2025, 1087, 3032]
"""All the items gained from ranking, indexed by their rank"""
STAMP_AWARDS = {1: 278, 3: 282, 4: 284}
STAMP_AWARDS = {
1: WaterStamp.WATER_MIDWAY,
3: WaterStamp.WATER_SUIT,
4: WaterStamp.WATER_NINJA,
}
"""Map rank and the stamp you gain from LEAVING the rank"""
board_cycle_handler: WaterCycleHandler
@ -469,6 +491,13 @@ class CardJitsuWaterLogic(IWaddle):
# original)
self.board = Board(columns=5 if len(waddle.penguins) <= 2 else 7)
async def remove_penguin(self, p: Penguin):
player = self.get_player_by_penguin(p)
# this may run twice for the same player
if player is not None:
self.players[player.seat_id].left = True
await super().remove_penguin(p)
async def send_zm(self, *args):
"""Send a "zm" packet, used for various commands, to the clients"""
await self.send_xt("zm", "&".join(map(str, args)))
@ -477,9 +506,11 @@ class CardJitsuWaterLogic(IWaddle):
"""Send a "zm" packet, used for various commands, to a specific client"""
await player.penguin.send_xt("zm", "&".join(map(str, args)))
def get_player_by_penguin(self, penguin: Penguin) -> WaterPlayer:
def get_player_by_penguin(self, penguin: Penguin) -> Union[WaterPlayer, None]:
"""Get the player instance associated with a penguin"""
return next(player for player in self.players if player.penguin == penguin)
return next(
(player for player in self.players if player.penguin == penguin), None
)
def get_card_generator(self, p: Penguin) -> Generator[WaterCard, None, None]:
"""Get a generator for a player's cards"""
@ -634,15 +665,12 @@ class CardJitsuWaterLogic(IWaddle):
).apply()
if winner.penguin.water_matches_won >= 100:
# Water Expert stamp
await winner.penguin.add_stamp(winner.penguin.server.stamps[276])
await winner.penguin.add_card_jitsu_stamp(WaterStamp.WATER_EXPERT)
# Gong! stamp
await winner.penguin.add_stamp(winner.penguin.server.stamps[270])
await winner.penguin.add_card_jitsu_stamp(WaterStamp.GONG)
if winner.two_close >= 2:
# Two Close stamp
await winner.penguin.add_stamp(winner.penguin.server.stamps[286])
await winner.penguin.add_card_jitsu_stamp(WaterStamp.TWO_CLOSE)
# iterate over all players that drowned from the last place order
for row in self.board.rows:
@ -650,7 +678,7 @@ class CardJitsuWaterLogic(IWaddle):
break
players_in_row = self.get_players_in_row(row)
for player in players_in_row:
if isinstance(player, WaterSensei):
if isinstance(player, WaterSensei) or player.left:
continue
# because winner has already been removed
@ -684,7 +712,7 @@ class CardJitsuWaterLogic(IWaddle):
p.server.items[cls.ITEM_AWARDS[rank]], cost=0, notify=False
)
if rank in cls.STAMP_AWARDS:
await p.add_stamp(p.server.stamps[cls.STAMP_AWARDS[rank]])
await p.add_card_jitsu_stamp(cls.STAMP_AWARDS[rank])
await p.update(water_ninja_rank=p.water_ninja_rank + ranks).apply()
return True
@ -700,20 +728,28 @@ class CardJitsuWaterLogic(IWaddle):
players_in_row = self.get_players_in_row(drop_row)
position = len(self.players)
for player in players_in_row:
if player.penguin is not None:
if not player.left:
# Watery Fall stamp
await player.penguin.add_stamp(player.penguin.server.stamps[274])
await player.penguin.add_card_jitsu_stamp(WaterStamp.WATERY_FALL)
# CMD_PLAYER_KILL, meant for players who lose from falling
player_kill_data = []
for player in players_in_row:
amulet = await self.update_player_progress(
player, fell=True, position=position
)
amulet = None
if not player.left:
amulet = await self.update_player_progress(
player, fell=True, position=position
)
else:
amulet = Amulet(None, False)
player_kill_data.append(
f"pk&{player.seat_id}&{position}&{amulet.serialize()}&false"
)
await self.send_zm(":".join(player_kill_data))
# for Two Close stamp
@ -985,12 +1021,15 @@ def get_water_rank_threshold(rank):
async def handle_get_game(p: Penguin):
"""Handle the client entering the game"""
seat_id = p.waddle.get_seat_id(p)
player = p.waddle.get_player_by_penguin(p)
player: WaterPlayer = p.waddle.get_player_by_penguin(p)
# needs to send these or the client dies
await p.send_xt("gz")
await p.send_xt("jz")
# needed to fix client taking a bit to exit game
await p.send_card_jitsu_stamp_info()
# CMD_PLAYER_INDEX
await p.waddle.send_zm_client(player, "po", seat_id)
@ -1109,8 +1148,7 @@ async def handle_throw_card(p: Penguin, *, cell_id: str):
)
if player.cleared >= 28:
# Skipping Stones stamp
await player.penguin.add_stamp(player.penguin.server.stamps[288])
await p.add_card_jitsu_stamp(WaterStamp.SKIPPING_STONES)
# CMD_PLAYER_THROW
await p.waddle.send_zm(

View File

@ -244,6 +244,8 @@ class Penguin(Spheniscidae, penguin.Penguin):
if stamp.id in self.stamps:
return False
await self.server.redis.set(self.get_recent_stamp_key(stamp.id), 1)
await self.stamps.insert(stamp_id=stamp.id)
if notify:
@ -254,6 +256,10 @@ class Penguin(Spheniscidae, penguin.Penguin):
return True
def get_recent_stamp_key(self, stamp_id):
"""Get redis key that locates the session recency of a stamp"""
return f'{self.id}.{stamp_id}.recentstamp'
async def add_inbox(self, postcard, sender_name="sys", sender_id=None, details=""):
penguin_postcard = await PenguinPostcard.create(penguin_id=self.id, sender_id=sender_id,
postcard_id=postcard.id, details=details)
@ -380,7 +386,90 @@ class Penguin(Spheniscidae, penguin.Penguin):
self.logger.info(f'{self.username} updated their background to \'{item.name}\' ' if item else
f'{self.username} removed their background item')
async def send_card_jitsu_stamp_info(self):
"""
Send information the client requires to properly display the stamp end screen
"""
stamp_info = await self.get_game_end_stamps_info(False)
await self.send_xt("cjsi", *stamp_info)
async def add_card_jitsu_stamp(self, stamp_id):
"""Correct way of adding a card-jitsu (Regular, Fire, Water) stamp"""
await self.add_stamp(self.server.stamps[stamp_id])
await self.send_card_jitsu_stamp_info()
async def get_game_end_stamps_info(
self, clear_session: bool
) -> tuple[str, int, int, int]:
"""
Get the info of the stamps at the end of a game that the client requires
If clear_session is True, the stamps will be marked off and will no longer
show up as new stamps at the end of minigames
"""
(
collected_game_stamps_string,
total_collected_game_stamps,
total_game_stamps,
total_stamps,
) = ("", 0, 0, 0)
game_stamps = [
stamp
for stamp in self.server.stamps.values()
if stamp.group_id == self.room.stamp_group
]
game_stamps_ids = [stamp.id for stamp in game_stamps]
recently_collected_game_stamps = []
for stamp in self.stamps.values():
if stamp.stamp_id in game_stamps_ids:
is_recent = await self.server.redis.get(
self.get_recent_stamp_key(stamp.stamp_id)
)
if is_recent:
recently_collected_game_stamps.append(stamp)
collected_game_stamps = [
stamp for stamp in game_stamps if (stamp.id in self.stamps and stamp)
]
collected_game_stamps_string = "|".join(
str(stamp.stamp_id) for stamp in recently_collected_game_stamps
)
total_collected_game_stamps = len(collected_game_stamps)
total_game_stamps = len(game_stamps)
total_stamps = len(
[
stamp
for stamp in self.stamps.values()
if self.server.stamps[stamp.stamp_id].group_id
]
)
if clear_session:
await self.clear_stamps_session()
return (
collected_game_stamps_string,
total_collected_game_stamps,
total_game_stamps,
total_stamps,
)
async def clear_stamps_session(self):
"""
Exits a game session and unmarks all stamps since we are no longer in their session
"""
async for key in self.server.redis.scan_iter(self.get_recent_stamp_key("*")):
await self.server.redis.delete(key)
def __repr__(self):
if self.id is not None:
return f'<Penguin ID=\'{self.id}\' Username=\'{self.username}\'>'