-
Notifications
You must be signed in to change notification settings - Fork 0
/
bot.py
432 lines (385 loc) · 15.4 KB
/
bot.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
import logging
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (
CallbackQueryHandler,
ChosenInlineResultHandler,
CommandHandler,
Filters,
InlineQueryHandler,
MessageHandler,
Updater,
)
from telegram.ext.callbackcontext import CallbackContext
from telegram.update import Update
from card import PURPLE, PURPLE_CARD, YELLOW, YELLOW_CARD, Card
from config import MIN_PLAYERS, TOKEN, WORKERS
from errors import (
AlreadyJoinedError,
DeckEmptyError,
LobbyClosedError,
NoGameInChatError,
NotEnoughPlayersError,
TooManyCardsError,
)
from game_manager import GameManager
from results import (
add_cards,
add_gameinfo,
add_no_game,
add_not_started,
add_purple_cards,
)
from utils import (
display_name,
make_card_players,
make_current_settlement,
make_game_start,
make_loser_discard,
make_other_players_notif,
make_room_info,
make_settlement,
)
logging.basicConfig(
format="%(asctime)s - %(filename)s:%(lineno)d - %(levelname)s - %(message)s",
level=logging.INFO,
)
logger = logging.getLogger(__name__)
class Room:
def __init__(self, updater: Updater):
self.updater = updater
self.handlers = [
InlineQueryHandler(self.reply_query, run_async=True),
ChosenInlineResultHandler(self.process_result, run_async=True),
CallbackQueryHandler(self.reply_callback, run_async=True),
CommandHandler("new", self.new, run_async=True),
CommandHandler("kill", self.kill, run_async=True),
CommandHandler("join", self.join, run_async=True),
CommandHandler("leave", self.leave, run_async=True),
CommandHandler("start", self.start, run_async=True),
CommandHandler("info", self.info, run_async=True),
MessageHandler(Filters.status_update, self.leave_group, run_async=True),
]
self.register()
def register(self):
for handler in self.handlers:
self.updater.dispatcher.add_handler(handler)
self.updater.dispatcher.add_error_handler(self.error)
def info(self, update: Update, context: CallbackContext):
chat = update.message.chat
if chat.type == "private":
return
games = gm.chatid_games.get(chat.id)
if games:
game = games[-1]
if game.ended:
update.message.reply_text("遊戲已結束!")
else:
update.message.reply_text(make_room_info(game))
else:
update.message.reply_text("目前沒有房間!請用 /new 開房!")
def new(self, update: Update, context: CallbackContext):
chat = update.message.chat
if chat.type == "private":
return
games = gm.chatid_games.get(chat.id)
if games:
game = games[-1]
if game.started and len(game.players) >= MIN_PLAYERS:
text = "已經開始ㄌ用 /join 中途插入ㄅ"
else:
text = "房間早就開ㄌ,用 /info 查看資訊,用 /join 加入"
update.message.reply_text(text)
return
game = gm.new_game(update.message.chat)
game.starter = update.message.from_user
update.message.reply_text("幫你開ㄌ,其他人可以用 /join 加入")
# NOTE: auto join
self.join(update, context)
def kill(self, update: Update, context: CallbackContext):
chat = update.message.chat
if chat.type == "private":
return
games = gm.chatid_games.get(chat.id)
if not games:
return
user = update.message.from_user
if user.username == "sheiun":
try:
gm.end_game(chat, user)
text = "遊戲終了!"
except NoGameInChatError:
return
else:
text = "你沒有權限"
update.message.reply_text(text)
def join(self, update: Update, context: CallbackContext):
chat = update.message.chat
if chat.type == "private":
return
try:
gm.join_game(update.message.from_user, chat)
except LobbyClosedError:
text = "關房了"
except NoGameInChatError:
text = "沒房ㄌ"
except AlreadyJoinedError:
text = "已經加入ㄌ拉"
except DeckEmptyError:
text = "牌不夠ㄌ"
else:
text = "加入成功ㄌ"
update.message.reply_text(text)
def leave(self, update: Update, context: CallbackContext):
chat = update.message.chat
user = update.message.from_user
player = gm.player_for_user_in_chat(user, chat)
if player is None:
text = "你不在遊戲內ㄡ"
else:
game = player.game
try:
gm.leave_game(user, chat)
except NoGameInChatError:
text = "你不在遊戲內ㄡ"
except NotEnoughPlayersError:
text = "遊戲結束ㄌ"
else:
if game.started:
text = f"好ㄉ。下位玩家 {display_name(game.current_player.user)}"
else:
text = f"{display_name(user)} 離開ㄌ"
update.message.reply_text(text)
def start(self, update: Update, context: CallbackContext):
chat = update.message.chat
if chat.type == "private":
return
text = ""
markup = None
try:
game = gm.chatid_games[chat.id][-1]
except (KeyError, IndexError):
text = "還沒開房ㄡ"
else:
if game.started:
text = "已經開始ㄌㄡ"
elif len(game.players) < MIN_PLAYERS:
text = f"至少要 {MIN_PLAYERS} 人才能開ㄡ"
else:
game.start()
text = make_game_start(game)
markup = choice
context.bot.send_message(chat.id, text=make_room_info(game))
update.message.reply_text(text, reply_markup=markup)
def leave_group(self, update: Update, context: CallbackContext):
chat = update.message.chat
if update.message.left_chat_member:
user = update.message.left_chat_member
try:
gm.leave_game(user, chat)
except NoGameInChatError:
return
except NotEnoughPlayersError:
gm.end_game(chat, user)
text = "遊戲終了!"
else:
text = display_name(user) + " 被踢出遊戲ㄌ"
context.bot.send_message(chat.id, text=text)
def reply_query(self, update: Update, context: CallbackContext):
results = []
user = update.inline_query.from_user
try:
player = gm.userid_current[user.id]
except KeyError:
add_no_game(results)
else:
game = player.game
if not game.started:
add_not_started(results)
elif user.id == game.current_player.user.id:
if game.board.purple is not None:
add_cards(results, player)
else:
add_purple_cards(results, game)
else:
add_cards(results, player)
update.inline_query.answer(results, cache_time=0)
def process_result(self, update: Update, context: CallbackContext):
user = update.chosen_inline_result.from_user
result_id = update.chosen_inline_result.result_id
try:
player = gm.userid_current[user.id]
game = player.game
chat = game.chat
except (KeyError, AttributeError):
return
if result_id in ("hand", "gameinfo", "nogame"):
return
if result_id.isdigit() and len(result_id) == 19:
# NOTE: play a yellow card
card = Card.from_id(result_id)
if game.state == game.State.PURPLE:
if player != game.current_player:
context.bot.send_message(
chat.id,
text=display_name(user) + "你現在不能出黃牌",
reply_to_message_id=update.chosen_inline_result.inline_message_id,
)
elif game.state == game.State.YELLOW:
if player != game.current_player:
try:
player.play(card)
except TooManyCardsError:
context.bot.send_message(
chat.id,
text=display_name(user) + "你出太多張了!",
reply_to_message_id=update.chosen_inline_result.inline_message_id,
)
return
# TODO: 需要提醒打牌成功?
# TODO: 如果 game.state 沒跳 show 打牌進度?
if game.state == game.State.LOSE:
context.bot.send_message(
chat.id, text=f"這張 {PURPLE_CARD} 是剛剛的題目"
)
context.bot.send_sticker(
chat.id, sticker=game.board.purple.sticker["file_id"]
)
for idx, cards in game.board.get_cards():
button = InlineKeyboardButton(
text=f"🔼 第 {idx} 組 🔼", callback_data=str(idx)
)
last_card = cards.pop()
for card in cards:
context.bot.send_sticker(
chat.id, sticker=card.sticker["file_id"]
)
context.bot.send_sticker(
chat.id,
sticker=last_card.sticker["file_id"],
reply_markup=InlineKeyboardMarkup([[button]]),
)
context.bot.send_message(
chat.id,
text=display_name(game.current_player.user)
+ "\n"
+ "請挑最爛ㄉ",
)
else:
context.bot.send_message(
chat.id,
text=display_name(user) + "你現在不能出黃牌",
reply_to_message_id=update.chosen_inline_result.inline_message_id,
)
elif game.state == game.State.DISCARD:
if player == game.board.loser:
player.discard(card)
if player.discarded:
game.turn()
context.bot.send_message(
chat.id,
text="換下一位玩家 " + display_name(game.current_player.user),
reply_markup=choice,
)
else:
context.bot.send_message(
chat.id,
text=display_name(user) + "你現在不能出黃牌",
reply_to_message_id=update.chosen_inline_result.inline_message_id,
)
elif result_id.startswith(PURPLE):
card = Card.from_id(result_id.replace(PURPLE, ""))
player.play(card)
# NOTE: notify other players to play yellow cards
context.bot.send_message(
chat.id, text=make_other_players_notif(game), reply_markup=choice
)
else:
logger.info(f"Result: {result_id} is run into else clause!")
# The card cannot be played
def reply_callback(self, update: Update, context: CallbackContext):
chat = update.callback_query.message.chat
user = update.callback_query.from_user
try:
player = gm.userid_current[user.id]
game = player.game
except (KeyError, AttributeError):
return
data = update.callback_query.data
if data.isdigit():
if player != game.current_player:
update.callback_query.answer("不要亂按啦!")
return
num = int(data)
# NOTE: resolve join midway problem
try:
loser = game.board.get_loser(num)
except IndexError:
return
if game.board.loser:
update.callback_query.answer("你已經選過了!")
return
loser.score -= game.board.purple.space
game.board.loser = loser
context.bot.send_message(chat.id, text=make_card_players(game, num))
# NOTE: Game end check
if game.get_loser():
gm.end_game(chat, user)
context.bot.send_message(chat.id, text=make_settlement(game))
return
context.bot.send_message(chat.id, text=make_current_settlement(game))
# NOTE: let loser to discard cards
game.state += 1
logger.info(f"{game.state} == {game.State.DISCARD}")
actions = [1, 2, "不換"]
buttons = []
for act in actions:
if isinstance(act, int):
data = f"discard{act}"
else:
data = "skip_discard"
buttons.append(InlineKeyboardButton(str(act), callback_data=data))
context.bot.send_message(
chat.id,
text=make_loser_discard(game),
reply_markup=InlineKeyboardMarkup([buttons]),
)
elif data.startswith("discard"):
if player != game.board.loser:
update.callback_query.answer("不要亂按啦!")
return
amount = int(data.replace("discard", ""))
if player.discard_amount > 0:
update.callback_query.answer(f"你已經選擇棄掉 {amount} 張 {YELLOW_CARD} 無法反悔")
return
player.discard_amount = amount
context.bot.send_message(
chat.id,
text=display_name(user) + f"選擇了棄掉 {amount} 張牌",
reply_markup=choice,
)
update.callback_query.answer(f"接下來請你選擇 {amount} 張 {YELLOW_CARD} 棄牌")
elif data == "skip_discard":
if player == game.board.loser and player.discard_amount == 0:
game.turn()
context.bot.send_message(
chat.id,
text="換下一位玩家 " + display_name(game.current_player.user),
reply_markup=choice,
)
text = "你選擇了不換牌!"
else:
text = "不要亂按啦!"
update.callback_query.answer(text)
else:
logger.info(f"{data} run into else clause")
def error(self, update: Update, context: CallbackContext):
"""Simple error handler"""
logger.exception(context.error)
def launch(self):
self.updater.start_polling()
self.updater.idle()
choice = InlineKeyboardMarkup(
[[InlineKeyboardButton("選牌!", switch_inline_query_current_chat="")]]
)
gm = GameManager()
Room(Updater(token=TOKEN, workers=WORKERS)).launch()