This file is indexed.

/usr/share/kde4/apps/kajongg/player.py is in kajongg 4:4.13.0-0ubuntu1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  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
# -*- coding: utf-8 -*-

"""
Copyright (C) 2008-2014 Wolfgang Rohdewald <wolfgang@rohdewald.de>

Kajongg is free software you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""

import weakref
from collections import defaultdict

from log import logException, logWarning, m18n, m18nc, m18nE
from common import WINDS, IntDict, Debug
from query import Query
from tile import Tile, TileList, elements
from meld import Meld, MeldList
from permutations import Permutations
from message import Message
from hand import Hand
from intelligence import AIDefault

class Players(list):
    """a list of players where the player can also be indexed by wind.
    The position in the list defines the place on screen. First is on the
    screen bottom, second on the right, third top, forth left"""

    allNames = {}
    allIds = {}
    humanNames = {}

    def __init__(self, players=None):
        list.__init__(self)
        if players:
            self.extend(players)

    def __getitem__(self, index):
        """allow access by idx or by wind"""
        for player in self:
            if player.wind == index:
                return player
        return list.__getitem__(self, index)

    def __str__(self):
        return ', '.join(list('%s: %s' % (x.name, x.wind) for x in self))

    def byId(self, playerid):
        """lookup the player by id"""
        for player in self:
            if player.nameid == playerid:
                return player
        logException("no player has id %d" % playerid)

    def byName(self, playerName):
        """lookup the player by name"""
        for player in self:
            if player.name == playerName:
                return player
        logException("no player has name %s - we have %s" % (playerName, [x.name for x in self]))

    @staticmethod
    def load():
        """load all defined players into self.allIds and self.allNames"""
        Players.allIds = {}
        Players.allNames = {}
        for nameid, name in Query("select id,name from player").records:
            Players.allIds[name] = nameid
            Players.allNames[nameid] = name
            if not name.startswith('Robot'):
                Players.humanNames[nameid] = name

    @staticmethod
    def createIfUnknown(name):
        """create player in database if not there yet"""
        if name not in Players.allNames.values():
            Players.load()  # maybe somebody else already added it
            if name not in Players.allNames.values():
                Query("insert or ignore into player(name) values(?)", (name,))
                Players.load()
        assert name in Players.allNames.values(), '%s not in %s' % (name, Players.allNames.values())

    def translatePlayerNames(self, names):
        """for a list of names, translates those names which are english
        player names into the local language"""
        known = set(x.name for x in self)
        return list(self.byName(x).localName if x in known else x for x in names)

class Player(object):
    """all player related attributes without GUI stuff.
    concealedTiles: used during the hand for all concealed tiles, ungrouped.
    concealedMelds: is empty during the hand, will be valid after end of hand,
    containing the concealed melds as the player presents them."""
    # pylint: disable=too-many-instance-attributes,too-many-public-methods

    def __init__(self, game):
        if game:
            self._game = weakref.ref(game)
        else:
            self._game = None
        self.__balance = 0
        self.__payment = 0
        self.wonCount = 0
        self.__name = ''
        self.wind = WINDS[0]
        self.intelligence = AIDefault(self)
        self.visibleTiles = IntDict(game.visibleTiles) if game else IntDict()
        self.handCache = {}
        self.cacheHits = 0
        self.cacheMisses = 0
        self.clearHand()
        self.__lastSource = '1' # no source: blessing from heaven or earth
        self.handBoard = None

    def clearCache(self):
        """clears the cache with Hands"""
        if Debug.hand and len(self.handCache):
            self.game.debug('%s: cache hits:%d misses:%d' % (self, self.cacheHits, self.cacheMisses))
        self.handCache.clear()
        Permutations.cache.clear()
        self.cacheHits = 0
        self.cacheMisses = 0

    @property
    def name(self):
        """write once, read many"""
        return self.__name

    @name.setter
    def name(self, value):
        """write once"""
        assert self.__name == ''
        assert value
        self.__name = value

    @property
    def game(self):
        """hide the fact that this is a weakref"""
        if self._game:
            return self._game()

    def clearHand(self):
        """clear player attributes concerning the current hand"""
        self._concealedTiles = []
        self._exposedMelds = []
        self._concealedMelds = []
        self._bonusTiles = []
        self.discarded = []
        self.visibleTiles.clear()
        self.newHandContent = None
        self.originalCallingHand = None
        self.__lastTile = None
        self.lastSource = '1'
        self.lastMeld = Meld()
        self.__mayWin = True
        self.__payment = 0
        self.originalCall = False
        self.dangerousTiles = list()
        self.claimedNoChoice = False
        self.playedDangerous = False
        self.usedDangerousFrom = None
        self.isCalling = False
        self.clearCache()
        self._hand = None

    @property
    def lastTile(self):
        """temp for debugging"""
        return self.__lastTile

    @lastTile.setter
    def lastTile(self, value):
        """temp for debugging"""
        assert isinstance(value, (Tile, type(None))), value
        self.__lastTile = value

    def invalidateHand(self):
        """some source for the computation of current hand changed"""
        self._hand = None

    @property
    def hand(self):
        """a readonly tuple"""
        if not self._hand:
            self._hand = self.computeHand()
        return self._hand

    @property
    def bonusTiles(self):
        """a readonly tuple"""
        return tuple(self._bonusTiles)

    @property
    def concealedTiles(self):
        """a readonly tuple"""
        return tuple(self._concealedTiles)

    @property
    def exposedMelds(self):
        """a readonly tuple"""
        return tuple(self._exposedMelds)

    @property
    def concealedMelds(self):
        """a readonly tuple"""
        return tuple(self._concealedMelds)

    @property
    def mayWin(self):
        """winning possible?"""
        return self.__mayWin

    @mayWin.setter
    def mayWin(self, value):
        """winning possible?"""
        if self.__mayWin != value:
            self.__mayWin = value
            self._hand = None

    @property
    def lastSource(self):
        """the source of the last tile the player got"""
        return self.__lastSource

    @lastSource.setter
    def lastSource(self, lastSource):
        """the source of the last tile the player got"""
        self.__lastSource = lastSource
        if lastSource == 'd' and not self.game.wall.living:
            self.__lastSource = 'Z'
        if lastSource == 'w' and not self.game.wall.living:
            self.__lastSource = 'z'

    @property
    def nameid(self):
        """the name id of this player"""
        return Players.allIds[self.name]

    @property
    def localName(self):
        """the localized name of this player"""
        return m18nc('kajongg, name of robot player, to be translated', self.name)

    @property
    def handTotal(self):
        """the hand total of this player for the final scoring"""
        if not self.game.winner:
            return 0
        else:
            return self.hand.total()

    @property
    def balance(self):
        """the balance of this player"""
        return self.__balance

    @balance.setter
    def balance(self, balance):
        """the balance of this player"""
        self.__balance = balance
        self.__payment = 0

    def getsPayment(self, payment):
        """make a payment to this player"""
        self.__balance += payment
        self.__payment += payment

    @property
    def payment(self):
        """the payments for the current hand"""
        return self.__payment

    @payment.setter
    def payment(self, payment):
        """the payments for the current hand"""
        assert payment == 0
        self.__payment = 0

    def __repr__(self):
        return u'{name:<10} {wind}'.format(name=self.name[:10], wind=self.wind)

    def __unicode__(self):
        return u'{name:<10} {wind}'.format(name=self.name[:10], wind=self.wind)

    def pickedTile(self, deadEnd, tileName=None):
        """got a tile from wall"""
        self.game.activePlayer = self
        tile = self.game.wall.deal([tileName], deadEnd=deadEnd)[0]
        if hasattr(tile, 'tile'):
            self.lastTile = tile.tile
        else:
            self.lastTile = tile
        self.addConcealedTiles([tile])
        if deadEnd:
            self.lastSource = 'e'
        else:
            self.game.lastDiscard = None
            self.lastSource = 'w'
        return self.lastTile

    def removeTile(self, tile):
        """remove from my tiles"""
        if tile.isBonus:
            self._bonusTiles.remove(tile)
        else:
            try:
                self._concealedTiles.remove(tile)
            except ValueError:
                raise Exception('removeTile(%s): tile not in concealed %s' % \
                    (tile, ''.join(self._concealedTiles)))
        if tile is self.lastTile:
            self.lastTile = None
        self._hand = None

    def addConcealedTiles(self, tiles, animated=False): # pylint: disable=unused-argument
        """add to my tiles"""
        assert len(tiles)
        for tile in tiles:
            assert isinstance(tile, Tile), 'tile:%s' % tile
            if tile.isBonus:
                self._bonusTiles.append(tile)
            else:
                assert tile.isConcealed, '%s data=%s' % (tile, tiles)
                self._concealedTiles.append(tile)
        self._hand = None

    def syncHandBoard(self, adding=None):
        """virtual: synchronize display"""
        pass

    def colorizeName(self):
        """virtual: colorize Name on wall"""
        pass

    def getsFocus(self, dummyResults=None):
        """virtual: player gets focus on his hand"""
        pass

    def mjString(self):
        """compile hand info into a string as needed by the scoring engine"""
        declaration = 'a' if self.originalCall else ''
        return ''.join(['m', self.lastSource, declaration])

    def makeTileKnown(self, tileName):
        """used when somebody else discards a tile"""
        assert not self._concealedTiles[0].isKnown
        self._concealedTiles[0] = tileName
        self._hand = None

    def computeHand(self, withTile=None):
        """returns Hand for this player"""
        assert not (self._concealedMelds and self._concealedTiles)
        assert isinstance(self.lastTile, (Tile, type(None)))
        assert isinstance(withTile, (Tile, type(None)))
        melds = ['R' + ''.join(str(x) for x in sorted(self._concealedTiles))]
        if withTile:
            melds[0] += withTile
        if melds[0] == 'R':
            melds = melds[1:]
        melds.extend(str(x) for x in self._exposedMelds)
        melds.extend(str(x) for x in self._concealedMelds)
        melds.extend(str(x) for x in self._bonusTiles)
        melds.append(self.mjString())
        if withTile or self.lastTile:
            melds.append('L%s%s' % (withTile or self.lastTile, self.lastMeld if self.lastMeld else ''))
        return Hand(self, ' '.join(melds))

    def scoringString(self):
        """helper for HandBoard.__str__"""
        if self._concealedMelds:
            parts = [str(x) for x in self._concealedMelds + self._exposedMelds]
        else:
            parts = [''.join(self._concealedTiles)]
            parts.extend([str(x) for x in self._exposedMelds])
        parts.extend(str(x) for x in self._bonusTiles)
        return ' '.join(parts)

    def sortRulesByX(self, rules): # pylint: disable=no-self-use
        """if this game has a GUI, sort rules by GUI order"""
        return rules

    def others(self):
        """a list of the other 3 players"""
        return (x for x in self.game.players if x != self)

    def tileAvailable(self, tileName, hand):
        """a count of how often tileName might still appear in the game
        supposing we have hand"""
        lowerTile = tileName.exposed
        upperTile = tileName.concealed
        visible = self.game.discardedTiles.count([lowerTile])
        if visible:
            if hand.lenOffset == 0 and self.game.lastDiscard and lowerTile is self.game.lastDiscard.exposed:
                # the last discarded one is available to us since we can claim it
                visible -= 1
        visible += sum(x.visibleTiles.count([lowerTile, upperTile]) for x in self.others())
        visible += sum(x.exposed == lowerTile for x in hand.tiles)
        return 4 - visible

    def violatesOriginalCall(self, discard=None):
        """called if discarding discard violates the Original Call"""
        if not self.originalCall or not self.mayWin:
            return False
        if self.lastTile.exposed != discard.exposed:
            if Debug.originalCall:
                self.game.debug('%s would violate OC with %s, lastTile=%s' % (self, discard, self.lastTile))
            return True
        return False

class PlayingPlayer(Player):
    """a player in a computer game as opposed to a ScoringPlayer"""
    # pylint: disable=too-many-public-methods
    # too many public methods
    def __init__(self, game):
        self.sayable = {}               # recompute for each move, use as cache
        Player.__init__(self, game)

    def popupMsg(self, msg):
        """virtual: show popup on display"""
        pass

    def hidePopup(self):
        """virtual: hide popup on display"""
        pass

    def speak(self, txt):
        """only a visible playing player can speak"""
        pass

    def declaredMahJongg(self, concealed, withDiscard, lastTile, lastMeld):
        """player declared mah jongg. Determine last meld, show concealed tiles grouped to melds"""
        melds = concealed[:]
        self.game.winner = self
        if withDiscard:
            assert isinstance(withDiscard, Tile), withDiscard
            PlayingPlayer.addConcealedTiles(self, [withDiscard]) # this should NOT invoke syncHandBoard
            if self.lastSource != 'k':   # robbed the kong
                self.lastSource = 'd'
            # the last claimed meld is exposed
            assert lastMeld in melds, '%s: melds=%s lastMeld=%s lastTile=%s withDiscard=%s' % (
                    self._concealedTiles, melds, lastMeld, lastTile, withDiscard)
            melds.remove(lastMeld)
            lastTile = withDiscard.exposed
            lastMeld = lastMeld.exposed
            self._exposedMelds.append(lastMeld)
            for tileName in lastMeld:
                self.visibleTiles[tileName] += 1
        self.lastTile = lastTile
        self.lastMeld = lastMeld
        self._concealedMelds = melds
        self._concealedTiles = []
        self._hand = None

    def __possibleChows(self):
        """returns a unique list of lists with possible claimable chow combinations"""
        if self.game.lastDiscard is None:
            return []
        exposedChows = [x for x in self._exposedMelds if x.isChow]
        if len(exposedChows) >= self.game.ruleset.maxChows:
            return []
        tile = self.game.lastDiscard
        within = TileList(self.concealedTiles[:])
        within.append(tile)
        return within.hasChows(tile)

    def __possibleKongs(self):
        """returns a unique list of lists with possible kong combinations"""
        kongs = []
        if self == self.game.activePlayer:
            # declaring a kong
            for tileName in set([x for x in self._concealedTiles if not x.isBonus]):
                if self._concealedTiles.count(tileName) == 4:
                    kongs.append(tileName.kong)
                elif self._concealedTiles.count(tileName) == 1 and \
                        tileName.exposed.pung in self._exposedMelds:
                    # the result will be an exposed Kong but the 4th tile
                    # came from the wall, so we use the form aaaA
                    kongs.append(tileName.kong.exposedClaimed)
        if self.game.lastDiscard:
            # claiming a kong
            discardTile = self.game.lastDiscard.concealed
            if self._concealedTiles.count(discardTile) == 3:
                # TODO: discard.kong.concealed is aAAa but we need AAAA
                kongs.append(Meld(discardTile * 4))
        return kongs

    def __maySayChow(self):
        """returns answer arguments for the server if calling chow is possible.
        returns the meld to be completed"""
        if self == self.game.nextPlayer():
            return self.__possibleChows()

    def __maySayPung(self):
        """returns answer arguments for the server if calling pung is possible.
        returns the meld to be completed"""
        lastDiscard = self.game.lastDiscard
        if self.game.lastDiscard:
            assert lastDiscard.isConcealed, lastDiscard
            if self.concealedTiles.count(lastDiscard) >= 2:
                return lastDiscard.pung

    def __maySayKong(self):
        """returns answer arguments for the server if calling or declaring kong is possible.
        returns the meld to be completed or to be declared"""
        return self.__possibleKongs()

    def __maySayMahjongg(self, move):
        """returns answer arguments for the server if calling or declaring Mah Jongg is possible"""
        game = self.game
        if move.message == Message.DeclaredKong:
            withDiscard = move.meld[0].concealed
        elif move.message == Message.AskForClaims:
            withDiscard = game.lastDiscard
        else:
            withDiscard = None
        hand = self.computeHand(withTile=withDiscard)
        if hand.won:
            if Debug.robbingKong:
                if move.message == Message.DeclaredKong:
                    game.debug('%s may rob the kong from %s/%s' % \
                    (self, move.player, move.exposedMeld))
            if Debug.mahJongg:
                game.debug('%s may say MJ:%s, active=%s' % (
                    self, list(x for x in game.players), game.activePlayer))
            return MeldList(x for x in hand.melds if not x.isDeclared), withDiscard, hand.lastMeld

    def __maySayOriginalCall(self):
        """returns True if Original Call is possible"""
        for tileName in set(self.concealedTiles):
            newHand = self.hand - tileName
            if newHand.callingHands:
                if Debug.originalCall:
                    self.game.debug('%s may say Original Call by discarding %s from %s' % (self, tileName, self.hand))
                return True

    def computeSayable(self, move, answers):
        """find out what the player can legally say with this hand"""
        self.sayable = {}
        for message in Message.defined.values():
            self.sayable[message] = True
        if Message.Pung in answers:
            self.sayable[Message.Pung] = self.__maySayPung()
        if Message.Chow in answers:
            self.sayable[Message.Chow] = self.__maySayChow()
        if Message.Kong in answers:
            self.sayable[Message.Kong] = self.__maySayKong()
        if Message.MahJongg in answers:
            self.sayable[Message.MahJongg] = self.__maySayMahjongg(move)
        if Message.OriginalCall in answers:
            self.sayable[Message.OriginalCall] = self.__maySayOriginalCall()

    def maybeDangerous(self, msg):
        """could answering with msg lead to dangerous game?
        If so return a list of resulting melds
        where a meld is represented by a list of 2char strings"""
        result = []
        if msg in (Message.Chow, Message.Pung, Message.Kong):
            possibleMelds = self.sayable[msg]
            if isinstance(possibleMelds[0], basestring):
                possibleMelds = [possibleMelds]
            result = [x for x in possibleMelds if self.mustPlayDangerous(x)]
        return result

    def hasConcealedTiles(self, tiles, within=None):
        """do I have those concealed tiles?"""
        if within is None:
            within = self._concealedTiles
        within = within[:]
        for tile in tiles:
            if tile not in within:
                return False
            within.remove(tile)
        return True

    def showConcealedTiles(self, tiles, show=True):
        """show or hide tiles"""
        if not self.game.playOpen and self != self.game.myself:
            if not isinstance(tiles, (list, tuple)):
                tiles = [tiles]
            assert len(tiles) <= len(self._concealedTiles), \
                '%s: showConcealedTiles %s, we have only %s' % (self, tiles, self._concealedTiles)
            for tileName in tiles:
                src, dst = (Tile.unknown, tileName) if show else (tileName, Tile.unknown)
                assert src != dst, (self, src, dst, tiles, self._concealedTiles)
                if not src in self._concealedTiles:
                    logException('%s: showConcealedTiles(%s): %s not in %s.' % \
                            (self, tiles, src, self._concealedTiles))
                idx = self._concealedTiles.index(src)
                self._concealedTiles[idx] = dst
            if self.lastTile and not self.lastTile.isKnown:
                self.lastTile = None
            self._hand = None
            self.syncHandBoard()

    def showConcealedMelds(self, concealedMelds, ignoreDiscard=None):
        """the server tells how the winner shows and melds his
        concealed tiles. In case of error, return message and arguments"""
        for meld in concealedMelds:
            for tile in meld:
                if tile == ignoreDiscard:
                    ignoreDiscard = None
                else:
                    if not tile in self._concealedTiles:
                        msg = m18nE('%1 claiming MahJongg: She does not really have tile %2')
                        return msg, self.name, tile
                    self._concealedTiles.remove(tile)
            if meld.isConcealed and not meld.isKong:
                self._concealedMelds.append(meld)
            else:
                self._exposedMelds.append(meld)
        if self._concealedTiles:
            msg = m18nE('%1 claiming MahJongg: She did not pass all concealed tiles to the server')
            return msg, self.name
        self._hand = None

    def robTile(self, tile):
        """used for robbing the kong"""
        assert tile.isConcealed
        tile = tile.exposed
        for meld in self._exposedMelds:
            if tile in meld:
                meld = meld.without(tile)
                self.visibleTiles[tile] -= 1
                break
        else:
            raise Exception('robTile: no meld found with %s' % tile)
        self.game.lastDiscard = tile.concealed
        self.lastTile = None  #  our lastTile has just been robbed
        self._hand = None

    def scoreMatchesServer(self, score):
        """do we compute the same score as the server does?"""
        if score is None:
            return True
        if any(not x.isKnown for x in self._concealedTiles):
            return True
        if str(self.hand) == score:
            return True
        self.game.debug('%s localScore:%s' % (self, self.hand))
        self.game.debug('%s serverScore:%s' % (self, score))
        logWarning('Game %s: client and server disagree about scoring, see logfile for details' % self.game.seed)
        return False

    def mustPlayDangerous(self, exposing=None):
        """returns True if the player has no choice, otherwise False.
        Exposing may be a meld which will be exposed before we might
        play dangerous"""
        if self == self.game.activePlayer and exposing and len(exposing) == 4:
            # declaring a kong is never dangerous because we get
            # an unknown replacement
            return False
        afterExposed = list(x.exposed for x in self._concealedTiles)
        if exposing:
            exposing = exposing[:]
            if self.game.lastDiscard:
                # if this is about claiming a discarded tile, ignore it
                # the player who discarded it is responsible
                exposing.remove(self.game.lastDiscard)
            for tile in exposing:
                if tile.exposed in afterExposed:
                    # the "if" is needed for claimed pung
                    afterExposed.remove(tile.exposed)
        return all(self.game.dangerousFor(self, x) for x in afterExposed)

    def exposeMeld(self, meldTiles, calledTile=None):
        """exposes a meld with meldTiles: removes them from concealedTiles,
        adds the meld to exposedMelds and returns it
        calledTile: we got the last tile for the meld from discarded, otherwise
        from the wall"""
        game = self.game
        game.activePlayer = self
        allMeldTiles = meldTiles[:]
        if calledTile:
            assert isinstance(calledTile, Tile), calledTile
            allMeldTiles.append(calledTile)
        if len(allMeldTiles) == 4 and allMeldTiles[0].isExposed:
            tile0 = allMeldTiles[0].exposed
            # we are adding a 4th tile to an exposed pung
            self._exposedMelds = [x for x in self._exposedMelds if x != tile0.pung]
            meld = tile0.kong
            if allMeldTiles[3] not in self._concealedTiles:
                game.debug('t3 %s not in conc %s' % (allMeldTiles[3], self._concealedTiles))
            self._concealedTiles.remove(allMeldTiles[3])
            self.visibleTiles[tile0] += 1
        else:
            allMeldTiles = sorted(allMeldTiles) # needed for Chow
            meld = Meld(allMeldTiles)
            for meldTile in meldTiles:
                self._concealedTiles.remove(meldTile)
            for meldTile in allMeldTiles:
                self.visibleTiles[meldTile.exposed] += 1
            meld = meld.exposedClaimed if calledTile else meld.declared
        if self.lastTile in allMeldTiles:
            self.lastTile = self.lastTile.exposed
        self._exposedMelds.append(meld)
        self._hand = None
        game.computeDangerous(self)
        return meld

    def findDangerousTiles(self):
        """update the list of dangerous tile"""
        pName = self.localName
        dangerous = list()
        expMeldCount = len(self._exposedMelds)
        if expMeldCount >= 3:
            if all(x in elements.greenHandTiles for x in self.visibleTiles):
                dangerous.append((elements.greenHandTiles,
                     m18n('Player %1 has 3 or 4 exposed melds, all are green', pName)))
            group = defaultdict.keys(self.visibleTiles)[0].group
            # see http://www.logilab.org/ticket/23986
            assert group.islower(), self.visibleTiles
            if group in Tile.colors:
                if all(x.group == group for x in self.visibleTiles):
                    suitTiles = set([Tile(group, x) for x in Tile.numbers])
                    if self.visibleTiles.count(suitTiles) >= 9:
                        dangerous.append((suitTiles, m18n('Player %1 may try a True Color Game', pName)))
                elif all(x.value in Tile.terminals for x in self.visibleTiles):
                    dangerous.append((elements.terminals,
                        m18n('Player %1 may try an All Terminals Game', pName)))
        if expMeldCount >= 2:
            windMelds = sum(self.visibleTiles[x] >= 3 for x in elements.winds)
            dragonMelds = sum(self.visibleTiles[x] >= 3 for x in elements.dragons)
            windsDangerous = dragonsDangerous = False
            if windMelds + dragonMelds == expMeldCount and expMeldCount >= 3:
                windsDangerous = dragonsDangerous = True
            windsDangerous = windsDangerous or windMelds >= 3
            dragonsDangerous = dragonsDangerous or dragonMelds >= 2
            if windsDangerous:
                dangerous.append((set(x for x in elements.winds if x not in self.visibleTiles),
                     m18n('Player %1 exposed many winds', pName)))
            if dragonsDangerous:
                dangerous.append((set(x for x in elements.dragons if x not in self.visibleTiles),
                     m18n('Player %1 exposed many dragons', pName)))
        self.dangerousTiles = dangerous
        if dangerous and Debug.dangerousGame:
            self.game.debug('dangerous:%s' % dangerous)