-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathplayer.py
More file actions
1642 lines (1384 loc) · 71.8 KB
/
player.py
File metadata and controls
1642 lines (1384 loc) · 71.8 KB
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
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from __future__ import annotations
from typing import List, TYPE_CHECKING
if TYPE_CHECKING:
from game import Game
from mr_cards import MrCard
from card import UnoCard
from util import PlayAction
from PyQt5.QtWidgets import QApplication
HAND_LIMIT = 20
class Player:
"""玩家基类,定义所有玩家共有的属性和方法"""
def __init__(self, position: int, team: str = None):
self.position = position
self.uno_list: List[UnoCard] = []
self.game: Game = None
self.mr_card: MrCard = None
self.team = team
self.uno_state = False # 添加uno状态,当手牌为1时激活
# ==================== 1. 基础属性和初始化 ====================
@property
def hand_limit(self):
"""获取玩家的手牌上限"""
if self.mr_card and any(s.name == '自守' for s in self.mr_card.skills):
return 8
return HAND_LIMIT
def update_uno_state(self):
"""更新uno状态:当手牌为1时激活,否则关闭"""
new_uno_state = len(self.uno_list) == 1
if new_uno_state != self.uno_state:
self.uno_state = new_uno_state
# 如果状态发生变化,可以在这里添加历史记录
try:
if self.game and new_uno_state:
self.game.add_history(f"{self.mr_card.name} 只剩一张手牌!")
elif self.game and not new_uno_state and self.uno_state:
# 从uno状态退出(手牌从1变为其他数量)
pass
except RuntimeError:
# 如果GUI组件已被删除,静默忽略
pass
# ==================== 2. 游戏逻辑核心方法 ====================
def turn(self):
"""
普通玩家回合:skip检查 -> +牌链处理 -> 出牌/摸牌/发动技能三选一 -> 根据打出牌类型更新flag -> 结束回合
"""
# 如果游戏已经结束,则不再继续
if self.game.game_over:
return
# 回合开始时的状态重置(原本在next_player方法中)
self.game.turn_action_taken = False
self.game.turn_count += 1
# 重置武圣状态(原本在next_player方法中)
if self.game.gui:
self.game.gui.wusheng_active = False
# 1. skip检查 - 检查当前玩家是否应该被跳过
if self._check_and_handle_skip():
return # 如果被跳过,回合结束
# 2. +牌链处理 - 处理强制摸牌
if self._handle_draw_chain():
# 如果处理了强制摸牌,回合已经结束,直接返回
return
# 3. 执行玩家回合内容
self.execute_turn_content()
# 4. 根据打出牌类型更新flag
self._update_flags_after_turn()
# 5. 回合结束时清空状态
self.game.clear_state()
def jump_turn(self):
"""
跳牌玩家特殊回合:可以打出与上一张牌相同的牌来跳牌
"""
# 如果游戏已经结束,则不再继续
if self.game.game_over:
return
print(f"跳牌玩家 {self.mr_card.name} 特殊回合开始")
# 跳牌玩家可以打出与上一张牌相同的牌来跳牌
action_type, action_value = self._get_jump_decision()
if action_type == 'play':
# 可以跳牌,执行出牌
self.play(action_value)
# 添加跳牌历史记录
# 获取跳牌时打出的牌
if action_value is not None and 0 <= action_value < len(self.uno_list):
jumped_card = self.uno_list[action_value]
# 可选详细日志:self.game.add_history(f"{self.mr_card.name} 跳牌使用: {jumped_card}")
else:
# 跳牌历史已由 Game.handle_gui_jump_turn 统一记录
pass
else:
# 无法跳牌,跳过这个特殊回合
print(f"{self.mr_card.name} 无法跳牌,跳过特殊回合")
# 更新flag(在跳牌回合中,reverse/skip不生效)
self._update_flags_after_jump_turn()
def _check_and_handle_skip(self):
"""检查并处理skip效果"""
if self.game.skip:
print(f"玩家 {self.position+1} ({self.mr_card.name}) 被跳过")
# 记录跳过历史
self.game.add_history(f"{self.mr_card.name} 被跳过!")
if self.game.gui:
# 在GUI中显示跳过信息
self.game.gui.show_temporary_message(f"玩家 {self.position + 1} ({self.mr_card.name}) 被跳过!")
self.game.skip = False # 消耗skip状态
# 设置行动标志,表示回合已结束(被跳过)
self.game.turn_action_taken = True
return True # 表示被跳过
return False # 表示未被跳过
def _handle_draw_chain(self):
"""处理+牌链(强制摸牌)"""
if self.game.draw_n > 0:
print(f"玩家 {self.position+1} 需要处理+牌链,摸 {self.game.draw_n} 张牌")
self.handle_forced_draw()
return True # 表示处理了强制摸牌
return False # 表示没有强制摸牌
def _settle_skip_and_draw_chain(self):
"""结算skip和+牌链(已废弃,保留用于兼容性)"""
# 检查skip效果
if self._check_and_handle_skip():
return # 如果被跳过,回合结束
# 处理+牌链
self._handle_draw_chain()
def _update_flags_after_turn(self):
"""根据打出牌类型更新flag"""
# 这个方法会在change_flag()中处理,这里只是占位
pass
def _update_flags_after_jump_turn(self):
"""跳牌回合后更新flag(reverse/skip不生效)"""
# 在跳牌回合中,reverse/skip效果不生效
last_play_info = self.game.playedcards.get_last_play_info()
if last_play_info:
effective_card, original_card, source_player = last_play_info
card_type = effective_card.type
if card_type == "reverse":
# 在跳牌回合中,reverse不生效,恢复方向
self.game.dir *= -1
print(f"跳牌回合中,reverse效果被忽略")
elif card_type == "skip":
# 在跳牌回合中,skip不生效,恢复skip状态
self.game.skip = False
print(f"跳牌回合中,skip效果被忽略")
def _get_jump_decision(self):
"""跳牌玩家决策:检查可以跳牌的牌"""
# 获取上一张牌
last_play_info = self.game.playedcards.get_last_play_info()
if not last_play_info:
return 'draw', None
last_card, _, _ = last_play_info
# 查找可以跳牌的牌
potential_jumps = self.check_for_jump(last_card)
if potential_jumps:
# 选择第一张可以跳牌的牌
jump_info = potential_jumps[0]
card_to_jump = jump_info['original_card']
# 找到这张牌在手牌中的索引
for i, card in enumerate(self.uno_list):
if card == card_to_jump:
return 'play', i
return 'draw', None
def execute_turn_content(self):
"""
执行一个玩家在一个回合内的全部内容,不包括跳牌与切换至下一个玩家
"""
# 如果游戏已经结束,则不再继续
if self.game.game_over:
return
# 1. 回合开始时的检查
# 检查恃才技能(UNO提醒)
self.check_shicai_skill()
# 回合开始时检查手牌上限
if len(self.uno_list) > self.hand_limit:
num_to_discard = len(self.uno_list) - self.hand_limit
cards_to_discard = self.choose_cards_to_discard(num_to_discard)
self.fold_card_objects([self.uno_list[i] for i in cards_to_discard])
discard_info = ', '.join(str(self.uno_list[i]) for i in cards_to_discard)
message = f"玩家 {self.position+1} ({self.mr_card.name}) 回合开始时手牌超限,弃置了: {discard_info}"
if self.game.gui:
self.game.gui.show_message_box("操作", message)
else:
print(message)
# 2. 处理强制摸牌(如果有的话)
if self.game.draw_n > 0:
print(f"玩家 {self.position+1} 摸 {self.game.draw_n} 张牌 (强制摸牌)")
# 显示摸牌提示
if self.game.gui:
self.game.gui.show_temporary_message(f"{self.mr_card.name} 摸了 {self.game.draw_n} 张牌", duration=1000)
self.handle_forced_draw()
# 强制摸牌完成后,检查是否有技能可以响应(如奸雄)
jianxiong_skill = next((s for s in self.mr_card.skills if s.__class__.__name__ == 'JianXiong'), None)
jianxiong_eligible_cards = [
original_card for effective_card, original_card, source_player in self.game.draw_chain_cards
if (effective_card.type == 'draw2' or effective_card.type == 'wild_draw4') and source_player != self
]
if jianxiong_skill and jianxiong_eligible_cards:
if self.game.ai_handler.decide_jianxiong(self, self.game.draw_chain_cards):
print(f"玩家 {self.position+1} 发动【奸雄】")
self.execute_skill_jianxiong()
# 清除摸牌状态
self.game.draw_n = 0
self.game.draw_chain_cards.clear()
# 设置保护:当前玩家因强制摸牌后,不允许立刻作为跳牌的第一候选
try:
self.game.skip_jump_after_forced_draw = True
self.game.player_who_just_forced_draw = self
except Exception:
pass
# 强制摸牌后,回合结束
self.game.turn_action_taken = True
return
# 3. 获取玩家决策(摸牌或出牌)
action_type, action_value = self._get_player_decision()
# 4. 执行玩家决策
self._execute_player_decision(action_type, action_value)
# 如果游戏已经结束,则不再继续
if self.game.game_over:
return
# 5. 处理出牌后的技能效果
if action_type == 'play' and self.game.turn_action_taken:
# 获取最后打出的牌
last_play_info = self.game.playedcards.get_last_play_info()
if last_play_info:
effective_card, original_card, source_player = last_play_info
if source_player == self:
self.handle_post_play_skills(effective_card)
# 6. 确保回合已结束(如果还没有设置的话)
if not self.game.turn_action_taken:
self.game.turn_action_taken = True
# ==================== 3. 跳牌相关 ====================
def handle_jump_logic(self) -> bool:
"""在出牌后检查并处理跳牌逻辑。返回 True 如果发生了跳牌,否则返回 False。"""
if not self.game:
return False
last_play_info = self.game.playedcards.get_last_play_info()
if not last_play_info:
return False
last_card, _, _ = last_play_info
players_to_check = self._get_players_to_check_for_jump()
for jumper in players_to_check:
if self._try_player_jump(jumper, last_card):
return True
return False
def check_for_jump(self, last_card: UnoCard) -> List:
"""默认的跳牌检查实现"""
potential_jumps = []
if not last_card:
return potential_jumps
for i, card in enumerate(self.uno_list):
# 1. 标准跳牌: 颜色、类型、数值完全一致(黑色牌不能跳牌)
if (card.color == last_card.color and card.type == last_card.type and card.value == last_card.value and
card.type not in ['wild', 'wild_draw4']):
potential_jumps.append({'original_card': card, 'virtual_card': None})
# 2. 武圣跳牌: 红色牌 跳 红色+2
if last_card.type == 'draw2' and last_card.color == 'red':
if self.mr_card and any(s.name == '武圣' for s in self.mr_card.skills):
if card.color == 'red' and card.type not in ['wild', 'wild_draw4']:
from card import UnoCard # 确保 UnoCard 被导入
virtual_card = UnoCard('draw2', 'red', 0)
potential_jumps.append({'original_card': card, 'virtual_card': virtual_card})
return potential_jumps
# ==================== 4. 摸牌相关(默认实现) ====================
def draw_cards(self, num_to_draw: int, from_deck: bool = True, specific_cards: List[UnoCard] = None, is_skill_draw: bool = False):
"""摸牌实现"""
if not self.game:
return
cards_drawn = []
if specific_cards:
cards_drawn = specific_cards
elif from_deck:
for _ in range(num_to_draw):
# 检查手牌上限,如果已达到上限则停止摸牌
if len(self.uno_list) >= self.hand_limit:
print(f"玩家 {self.position+1} ({self.mr_card.name}) 手牌已达上限({self.hand_limit}),停止摸牌。")
# 记录到历史:达到手牌上限,停止摸牌(技能摸牌也记录,因为这是重要的游戏状态)
try:
if self.game:
self.game.add_history(f"{self.mr_card.name} 手牌已达上限({self.hand_limit}),停止摸牌")
except RuntimeError:
# 如果GUI组件已被删除,静默忽略
pass
break
if self.game.unocard_pack:
cards_drawn.append(self.game.unocard_pack.pop())
else:
break
if not cards_drawn:
return
self.uno_list.extend(cards_drawn)
# 更新uno状态
self.update_uno_state()
# 只有非技能摸牌才显示print信息和历史记录
if not is_skill_draw:
print(f"玩家 {self.position+1} 获得了 {len(cards_drawn)} 张牌。")
# 历史记录:摸牌(技能发动的摸牌不记录,避免重复)
try:
if self.game:
self.game.add_history(f"{self.mr_card.name} 摸了 {len(cards_drawn)} 张牌")
except RuntimeError:
# 如果GUI组件已被删除,静默忽略
pass
# 统计:记录摸牌数量(技能摸牌也计入“摸了多少张”)
if self.game:
try:
self.game.record_draw(self, len(cards_drawn))
except Exception:
pass
# 摸牌后,立即检查手牌上限
self.check_hand_limit_and_discard_if_needed()
# 通知GUI有玩家摸牌
if self.game:
self.game.notify_cards_drawn(self, len(cards_drawn))
self.game.notify_player_hand_changed(self)
self.game.notify_draw_pile_changed()
def handle_forced_draw(self):
"""强制摸牌实现"""
# 先完成强制摸牌(遵守手牌上限)
actual_draw_n = min(self.game.draw_n, self.hand_limit - len(self.uno_list))
if actual_draw_n > 0:
self.draw_cards(actual_draw_n)
# 如果实际摸牌数少于要求的摸牌数,说明达到了手牌上限
if actual_draw_n < self.game.draw_n:
try:
if self.game:
self.game.add_history(f"{self.mr_card.name} 强制摸牌时达到手牌上限({self.hand_limit}),只摸了 {actual_draw_n} 张牌")
except RuntimeError:
# 如果GUI组件已被删除,静默忽略
pass
else:
# 如果已经达到手牌上限,记录到历史
try:
if self.game:
self.game.add_history(f"{self.mr_card.name} 手牌已达上限({self.hand_limit}),无法强制摸牌")
except RuntimeError:
# 如果GUI组件已被删除,静默忽略
pass
# 强制摸牌完成后,检查是否有技能可以响应(如奸雄)
jianxiong_skill = next((s for s in self.mr_card.skills if s.__class__.__name__ == 'JianXiong'), None)
if jianxiong_skill and self.game.draw_chain_cards:
# 检查是否为AI玩家
if isinstance(self, AIPlayer):
# AI玩家使用AI决策逻辑
if self.game.ai_handler.ai_choose_to_use_skill(self, '奸雄'):
self.execute_skill_jianxiong()
elif self.game.gui:
# 人类玩家通过GUI询问
if self.game.gui.ask_yes_no_question("发动奸雄", "是否发动【奸雄】获得所有[+2]/[+4]牌?"):
self.execute_skill_jianxiong()
self.game.draw_n = 0
self.game.draw_chain_cards.clear()
# 强制摸牌完成后,结束玩家的回合
self.game.turn_action_taken = True
# 统计:将本次强制摸牌归因到+牌来源玩家
try:
self.game.attribute_forced_draw(self, actual_draw_n)
except Exception:
pass
# ==================== 5. 出牌相关(默认实现) ====================
def play(self, card_idx: int, wusheng_active: bool = False):
"""出牌实现"""
is_valid, message, card_to_play, original_card = self.validate_play(card_idx, wusheng_active)
if not is_valid:
if self.game.gui:
self.game.gui.show_message_box("提示", message)
else:
print(message)
return
color_choice = None
if card_to_play.type in ['wild', 'wild_draw4']:
if isinstance(self, AIPlayer):
# AI玩家自动选择颜色,不需要对话框
color_choice = self._choose_ai_wild_color()
elif self.game.gui:
# 人类玩家通过对话框选择颜色
color_choice = self.game.gui.choose_color_dialog()
if not color_choice:
return # 玩家取消选择
else:
color_choice = 'red' # 非GUI模式默认为红色
# 对于reverse卡牌,需要特殊处理target_player
if original_card.type == 'reverse':
# 计算方向改变后的下一个玩家
new_dir = -self.game.dir # 方向改变后的方向
next_pos_after_reverse = (self.position + new_dir) % len(self.game.player_list)
target_player = self.game.player_list[next_pos_after_reverse]
else:
# 获取下一个玩家,但不切换回合
target_player = self.game._get_next_player(self.position)
action = PlayAction(
card=original_card,
source=self,
target=target_player,
color_choice=color_choice,
virtual_card=card_to_play if wusheng_active and original_card.color == 'red' else None
)
# 历史记录在process_play_action中统一处理
self.game.turn_action_taken = True
self.process_play_action(action)
# 检查是否是最后一张黑色牌,如果是则摸一张
if hasattr(self, '_last_card_is_black') and self._last_card_is_black:
self.draw_cards(1)
self.game.add_history(f"{self.mr_card.name} 打出最后一张黑色牌,摸了一张牌")
self._last_card_is_black = False # 重置标记
def validate_play(self, card_idx: int, wusheng_active: bool):
"""默认的出牌验证"""
if card_idx is None or card_idx >= len(self.uno_list):
return False, "无效的卡牌索引。", None, None
original_card = self.uno_list[card_idx]
card_to_play = original_card
if wusheng_active and original_card.color == 'red':
from card import UnoCard
card_to_play = UnoCard('draw2', 'red', 0)
# 检查是否为跳牌场景:如果是跳牌,则跳过普通出牌规则检查
is_jump_scenario = False
last_play_info = self.game.playedcards.get_last_play_info()
if last_play_info:
effective_card, _, _ = last_play_info
# 检查当前玩家是否可以跳牌
potential_jumps = self.check_for_jump(effective_card)
if potential_jumps:
# 检查选中的牌是否在可跳牌列表中
for jump_info in potential_jumps:
if jump_info['original_card'] == original_card:
is_jump_scenario = True
break
# 只有在非跳牌场景下才检查普通出牌规则
if not is_jump_scenario and not self.check_card(card_to_play):
return False, "这张牌不符合出牌规则。", None, None
if len(self.uno_list) == 2 and self.game.gui:
# 可以在这里添加喊UNO的逻辑
pass
# 最后一张牌是黑色牌的特殊处理:允许出牌,但出牌后需要摸一张
if len(self.uno_list) == 1 and card_to_play.type in ['wild', 'wild_draw4']:
# 标记这是最后一张黑色牌,需要在出牌后摸一张
self._last_card_is_black = True
return True, "有效出牌", card_to_play, original_card
def check_card(self, card: UnoCard):
"""默认的卡牌检查实现"""
last_card = self.game.playedcards.get_one()
cur_color = self.game.cur_color
# 检查+2/+4叠加规则:只有在draw_n > 0时才应用
if self.game.draw_n > 0 and last_card:
if last_card.type == 'draw2':
# +2上只能叠+2或+4
if card.type not in ['draw2', 'wild_draw4']:
return False
elif last_card.type == 'wild_draw4':
# +4上只能叠+4
if card.type != 'wild_draw4':
return False
# 检查倾国技能
if self.mr_card:
qingguo_skill = next((s for s in self.mr_card.skills if s.name == '倾国'), None)
if qingguo_skill and card.color == 'blue':
return True # 蓝色牌可以当任何颜色出
# 检查龙胆技能
longdan_skill = next((s for s in self.mr_card.skills if s.name == '龙胆'), None)
if longdan_skill:
if card.color == 'red' and cur_color == 'blue':
return True
if card.color == 'blue' and cur_color == 'red':
return True
if card.type == 'wild' or card.type == 'wild_draw4':
return True
if card.color == cur_color:
return True
if last_card and card.type == last_card.type and card.type != 'number':
return True
if last_card and card.type == 'number' and last_card.type == 'number' and card.value == last_card.value:
return True
return False
def can_play_any_card(self) -> bool:
"""检查手牌中是否有任何可以合法打出的牌"""
for card in self.uno_list:
if self.check_card(card):
return True
return False
def play_a_hand(self, i: int):
"""打出一张手牌"""
card = self.uno_list.pop(i)
# 更新uno状态
self.update_uno_state()
return card
def play_card_object(self, card: UnoCard):
"""打出卡牌对象"""
try:
self.uno_list.remove(card)
# 更新uno状态
self.update_uno_state()
# 通知GUI有玩家出牌
if self.game:
self.game.notify_card_played(self, card)
self.game.notify_player_hand_changed(self)
except ValueError:
# 静默处理,不显示错误对话框
pass
# ==================== 6. 弃牌相关(默认实现) ====================
def fold_card(self, indices):
"""默认的弃牌实现"""
# 确保indices是列表
if not isinstance(indices, list):
indices = [indices]
# 从大到小排序,防止删除时索引变化
indices.sort(reverse=True)
cards_folded = []
for i in indices:
if i < len(self.uno_list):
cards_folded.append(self.uno_list.pop(i))
# 更新uno状态
self.update_uno_state()
return cards_folded
def fold_card_objects(self, cards_to_fold: List[UnoCard]):
"""根据卡牌对象弃牌"""
cards_folded = []
for card in cards_to_fold:
try:
self.uno_list.remove(card)
cards_folded.append(card)
except ValueError:
if self.game and self.game.gui:
self.game.gui.show_message_box("警告", f"尝试弃掉不存在的牌 {card}")
else:
print(f"警告: 尝试弃掉不存在的牌 {card}")
return cards_folded
# ==================== 7. 技能相关(抽象方法) ====================
def execute_skill_jianxiong(self):
"""奸雄技能处理 - 子类需要重写"""
raise NotImplementedError("子类必须实现此方法")
def execute_skill_wusheng(self, card_idx):
"""武圣技能处理 - 子类需要重写"""
raise NotImplementedError("子类必须实现此方法")
def execute_skill(self, skill, *args):
"""通用技能执行 - 子类需要重写"""
raise NotImplementedError("子类必须实现此方法")
def activate_skill(self, skill_name: str):
"""激活技能 - 子类需要重写"""
raise NotImplementedError("子类必须实现此方法")
def handle_jump_card_effect(self, card):
"""处理跳牌时的卡片效果 - 默认实现"""
# 跳牌时直接应用效果,不延迟
if card.type == "draw2":
# 跳牌时如果跳的是+2牌,则清空加牌链并只保留当前+2牌
self.game.draw_n = 2
self.game.draw_chain_cards.clear()
self.game.draw_chain_cards.append((card, card, self))
elif card.type == "wild_draw4":
# 跳牌时如果跳的是+4牌,则清空加牌链并只保留当前+4牌
self.game.draw_n = 4
self.game.draw_chain_cards.clear()
self.game.draw_chain_cards.append((card, card, self))
# 跳牌时skip与reverse不生效,也不对当前flag做出改变
# elif card.type == "skip":
# self.game.skip = True
# elif card.type == "reverse":
# self.game.dir *= -1
# self.game.add_history("方向倒转!")
def handle_jump_skills(self, jump_card):
"""处理跳牌后的技能效果 - 子类需要重写"""
raise NotImplementedError("子类必须实现此方法")
def handle_post_play_skills(self, card):
"""处理出牌后技能 - 子类需要重写"""
raise NotImplementedError("子类必须实现此方法")
def _check_and_handle_skill(self, skill_name: str, handler_method: str, *args, skill_class_name: str = None, condition_func=None):
"""通用技能检查和处理方法"""
if not self.mr_card:
return False
# 查找技能
if skill_class_name:
skill = next((s for s in self.mr_card.skills if s.__class__.__name__ == skill_class_name), None)
else:
skill = next((s for s in self.mr_card.skills if s.name == skill_name), None)
# 检查条件函数
condition_met = True
if condition_func is not None:
# 检查条件函数的参数数量
import inspect
sig = inspect.signature(condition_func)
if len(sig.parameters) == 0:
# 无参数的条件函数(如lambda: card.color == 'green')
condition_met = condition_func()
else:
# 有参数的条件函数
condition_met = condition_func(*args)
if skill and condition_met:
if handler_method and hasattr(self, handler_method):
getattr(self, handler_method)(skill, *args)
return True
return False
# ==================== 8. 决策相关(抽象方法) ====================
def choose_cards_to_discard(self, num_to_discard: int) -> List[int]:
"""选择要弃置的卡牌 - 子类需要重写"""
raise NotImplementedError("子类必须实现此方法")
def choose_to_use_skill(self, skill_name: str) -> bool:
"""选择是否使用技能 - 子类需要重写"""
raise NotImplementedError("子类必须实现此方法")
def choose_blue_card_to_play_for_lord(self) -> UnoCard:
"""选择蓝色牌为主公打出 - 子类需要重写"""
raise NotImplementedError("子类必须实现此方法")
def _get_player_decision(self):
"""获取玩家决策 - 子类需要重写"""
raise NotImplementedError("子类必须实现此方法")
def _execute_player_decision(self, action_type, action_value):
"""执行玩家决策 - 子类需要重写"""
raise NotImplementedError("子类必须实现此方法")
def _handle_action_failure(self):
"""处理行动失败时的通用逻辑"""
if self.game.draw_n > 0:
self.handle_forced_draw()
self._check_jianxiong_after_draw()
self.game.draw_n = 0
self.game.draw_chain_cards.clear()
else:
self.draw_cards(1)
# 确保设置行动标志
self.game.turn_action_taken = True
def _check_jianxiong_after_draw(self):
"""摸牌后检查奸雄技能 - 子类可以重写"""
pass
# ==================== 9. 辅助方法 ====================
def check_hand_limit_and_discard_if_needed(self):
"""检查手牌是否超限"""
if len(self.uno_list) >= self.hand_limit:
if self.game and self.game.gui:
self.game.gui.show_temporary_message(f"{self.mr_card.name} 手牌已达上限,不能再摸牌!")
return True
return False
def _get_players_to_check_for_jump(self):
"""获取需要检查跳牌的玩家列表"""
if not self.game:
return []
players_to_check = []
start_pos = self.game.cur_location
current_pos = start_pos # 从当前玩家开始检查
actual_player_count = len(self.game.player_list)
while len(players_to_check) < actual_player_count: # 检查所有玩家
players_to_check.append(self.game.player_list[current_pos])
current_pos = (current_pos + self.game.dir) % actual_player_count
if current_pos == start_pos: # 如果回到起始位置,说明已经检查完所有玩家
break
return players_to_check
def _try_player_jump(self, jumper, last_card):
"""尝试让指定玩家跳牌"""
potential_jumps = jumper.check_for_jump(last_card)
if not potential_jumps:
return False
chosen_jump_info = potential_jumps[0]
perform_jump = self._decide_jump_action(jumper, chosen_jump_info)
if perform_jump:
self._execute_jump(jumper, chosen_jump_info)
return True
return False
def _decide_jump_action(self, jumper, chosen_jump_info):
"""决定是否执行跳牌"""
from player import AIPlayer
if isinstance(jumper, AIPlayer):
return True # AI总是跳牌
elif self.game and self.game.gui:
jump_card_display = str(chosen_jump_info['virtual_card'] or chosen_jump_info['original_card'])
return self.game.gui.ask_yes_no_question("发现跳牌机会", f"是否使用 {jump_card_display} 进行跳牌?")
return False
def _execute_jump(self, jumper, chosen_jump_info):
"""执行跳牌动作"""
if not self.game:
return
original_card = chosen_jump_info['original_card']
virtual_card = chosen_jump_info['virtual_card']
if self.game.gui:
self.game.gui.show_message_box("跳牌!", f"玩家 {jumper.position+1} ({jumper.mr_card.name}) 跳牌!")
# 重置+牌串
if self.game.draw_n > 0:
self.game.draw_n = 0
self.game.draw_chain_cards.clear()
# 添加跳牌历史记录(武圣跳牌有特殊的历史记录,这里不添加通用记录)
jump_card_for_history = virtual_card if virtual_card else original_card
# 检查是否是武圣跳牌,如果是则不添加通用跳牌历史记录
is_wusheng_jump = (virtual_card and virtual_card.type == 'draw2' and virtual_card.color == 'red')
if not is_wusheng_jump:
# 跳牌历史已由 Game.handle_gui_jump_turn 统一记录
pass
# 处理跳牌后的技能效果(在出牌前)
if virtual_card:
jumper.handle_jump_skills(virtual_card)
else:
jumper.handle_jump_skills(original_card)
# 处理跳牌的出牌动作
from util import PlayAction
action = PlayAction(
card=original_card,
source=jumper,
target=None, # 让process_play_action处理目标玩家
virtual_card=virtual_card
)
jumper.process_play_action(action, is_jump=True, original_player_position=self.game.cur_location)
def process_play_action(self, action, is_jump: bool = False, original_player_position: int = None):
"""处理一个出牌动作的核心函数。所有出牌逻辑都应通过这里。"""
if not self.game:
return
original_card = action.card
effective_card = action.virtual_card if action.virtual_card else original_card
# 1. 从玩家手牌中移除原始牌
self.play_card_object(original_card)
# 2. 将行动信息放入弃牌堆(跳牌时不添加)
if not is_jump:
self.game.playedcards.add_card(effective_card, self, original_card)
# 统计:记录玩家打出一张牌
try:
self.game.record_play(self)
except Exception:
pass
# 标记:发生了“真正的出牌”,打开跳牌窗口
try:
self.game.jump_window_open = True
except Exception:
pass
# 处理武圣技能的历史记录
if not getattr(self.game, 'suppress_next_play_history', False):
if effective_card.type == 'draw2' and effective_card.color == 'red' and original_card.color == 'red' and original_card.type != 'draw2':
# 这是武圣技能激活的情况
self.game.add_history(f"{self.mr_card.name} 发动[武圣]技能,将 [{original_card}] 当作 [红+2] 打出 -> {action.target.mr_card.name}")
else:
# 正常出牌的历史记录
self.game.add_history(f"{self.mr_card.name} - {original_card} -> {action.target.mr_card.name}")
else:
# 被跳牌逻辑抑制的第一条正常出牌历史,跳过并重置标志
self.game.suppress_next_play_history = False
else:
# 跳牌时,只添加弃牌堆信息,不添加历史记录(历史记录在_execute_jump中处理)
self.game.playedcards.add_card(effective_card, self, original_card)
# 3. 更新当前颜色
self._update_color_after_play(effective_card, action.color_choice)
# 4. 根据牌的效果更新游戏状态
self.game.change_flag(is_jump)
# 5. 检查胜利条件
if self.game.check_win_condition(self):
return
# 6. 处理出牌后可能触发的技能(优先处理)
if not is_jump:
self.handle_post_play_skills(original_card)
# 7. 出牌后的回合处理(技能处理完成后再处理)
if is_jump:
# 跳牌历史记录已经在_execute_jump中处理了,这里不需要重复处理
# 跳牌后自动结束回合,切换到下一个玩家
self.game.turn_action_taken = True # 标记回合已结束
self.game.clear_state()
self.game.turn_count += 1
# skip效果现在在回合开始时处理,这里不需要处理
# +2/+4效果应该在下一个玩家回合开始时处理,这里不处理
# 跳牌检查现在在游戏主循环中进行,这里不需要处理
else:
# 非跳牌的正常出牌逻辑
# 在设置turn_action_taken之前,先检查当前玩家是否有相同的牌可以跳牌
last_play_info = self.game.playedcards.get_last_play_info()
if last_play_info:
effective_card, original_card, source_player = last_play_info
# 检查当前玩家是否有相同的牌可以跳牌
potential_jumps = self.check_for_jump(effective_card)
if potential_jumps:
# 当前玩家有相同的牌可以跳牌,不结束回合,让游戏循环处理跳牌
if self.game.gui:
self.game.gui.restart_game_loop()
else:
# 当前玩家没有相同的牌可以跳牌,检查其他玩家是否有跳牌机会
if self.game.check_for_jump():
# 其他玩家有跳牌机会,不结束回合,让游戏循环处理跳牌
if self.game.gui:
self.game.gui.restart_game_loop()
else:
# 没有任何跳牌机会,正常结束回合
self.game.turn_action_taken = True # 标记回合已结束
self.game.clear_state()
self.game.turn_count += 1
else:
# 没有上一张牌信息,正常结束回合
self.game.turn_action_taken = True # 标记回合已结束
self.game.clear_state()
self.game.turn_count += 1
def _update_color_after_play(self, effective_card, color_choice):
"""更新出牌后的颜色"""
if effective_card.type in ['wild', 'wild_draw4']:
if color_choice:
# 如果已经有颜色选择(AI玩家或人类玩家已选择),直接使用
self.game.cur_color = color_choice
else:
# 如果没有颜色选择(不应该发生),使用默认逻辑
if isinstance(self, AIPlayer):
self.game.cur_color = self._choose_ai_wild_color()
else:
self.game.cur_color = self._choose_player_wild_color()
else:
self.game.cur_color = effective_card.color
def _choose_player_wild_color(self):
"""玩家选择万能牌颜色"""
if self.game and self.game.gui:
chosen_color = self.game.gui.choose_color_dialog()
if chosen_color:
return chosen_color
import random
return random.choice(['red', 'blue', 'yellow', 'green'])
def check_shicai_skill(self):
"""检查恃才技能(UNO提醒)"""
if not self.mr_card:
return
shicai_skill = next((s for s in self.mr_card.skills if s.__class__.__name__ == 'ShiCai'), None)
if shicai_skill:
result = shicai_skill.check_uno(self)
if result:
self.game.add_history(result)
if self.game.gui:
self.game.gui.show_temporary_message(result)
# ==================== 10. 入口方法 ====================
# 这些方法直接调用对应的player_*方法,保持接口一致性
class HumanPlayer(Player):
"""人类玩家类,继承自Player基类"""
def __init__(self, position: int, team: str = None):
super().__init__(position, team)
# ==================== 人类玩家特有的实现 ====================
# ==================== 技能相关 ====================
def execute_skill_jianxiong(self):
"""人类玩家奸雄技能处理"""
cards_to_gain = []
# draw_chain_cards现在存储的是(effective_card, original_card, source_player)元组
for _, original_card, _ in self.game.draw_chain_cards:
cards_to_gain.append(original_card)
for card in cards_to_gain:
self.uno_list.append(card) # 直接加入手牌,绕过player_draws_cards
# 更新uno状态
self.update_uno_state()
message = f"玩家 {self.position+1} 发动【奸雄】,获得了以下牌: {', '.join(str(c) for c in cards_to_gain)}"
if self.game.gui:
self.game.gui.show_message_box("技能发动", message)
else:
print(message)
# 添加历史记录
self.game.add_history(f"{self.mr_card.name} 发动[奸雄],获得了 {len(cards_to_gain)} 张牌")
# 获得牌后,检查手牌上限
self.check_hand_limit_and_discard_if_needed()
self.game.draw_n = 0 # 罚牌数清零
self.game.draw_chain_cards.clear() # 清空+牌串
# 奸雄后应该结束回合
self.game.turn_action_taken = True
# 奸雄后是自己的回合,所以不需要next_player,只需要刷新UI
if self.game.gui and not self.game.is_discard_mode:
self.game.gui.show_game_round()
def execute_skill_wusheng(self, card_idx):
"""人类玩家武圣技能处理"""
from card import UnoCard
original_card = self.uno_list.pop(card_idx)