1import asyncio
2from collections import defaultdict
3
4from pychess.Players.Player import Player, PlayerIsDead, PassInterrupt, TurnInterrupt, GameEnded
5from pychess.Utils.Move import parseSAN, toAN
6from pychess.Utils.lutils.lmove import ParsingError
7from pychess.Utils.Offer import Offer
8from pychess.Utils.const import REMOTE, UNFINISHED_STATES, CHAT_ACTION, CASTLE_KK, \
9    FISCHERRANDOMCHESS, CASTLE_SAN, TAKEBACK_OFFER
10from pychess.System.Log import log
11
12
13class ICPlayer(Player):
14    __type__ = REMOTE
15
16    def __init__(self,
17                 gamemodel,
18                 ichandle,
19                 gameno,
20                 color,
21                 name,
22                 icrating=None):
23        Player.__init__(self)
24        self.offers = {}
25        self.setName(name)
26        self.ichandle = ichandle
27        self.icrating = icrating
28        self.color = color
29        self.gameno = gameno
30        self.gamemodel = gamemodel
31        self.pass_interrupt = False
32        self.turn_interrupt = False
33
34        self.connection = connection = self.gamemodel.connection
35
36        self.connections = connections = defaultdict(list)
37        connections[connection.om].append(connection.om.connect(
38            "onOfferAdd", self.__onOfferAdd))
39        connections[connection.om].append(connection.om.connect(
40            "onOfferRemove", self.__onOfferRemove))
41        connections[connection.om].append(connection.om.connect(
42            "onOfferDeclined", self.__onOfferDeclined))
43        connections[connection.cm].append(connection.cm.connect(
44            "privateMessage", self.__onPrivateMessage))
45
46        self.cid = self.gamemodel.connect_after("game_terminated", self.on_game_terminated)
47
48    def on_game_terminated(self, model):
49        self.gamemodel.disconnect(self.cid)
50
51    def getICHandle(self):
52        return self.name
53
54    @property
55    def time(self):
56        return self.gamemodel.timemodel.getPlayerTime(self.color)
57
58    @property
59    def move_queue(self):
60        return self.gamemodel.ficsgame.move_queue
61
62    # Handle signals from the connection
63
64    def __onOfferAdd(self, om, offer):
65        if self.gamemodel.status in UNFINISHED_STATES and not self.gamemodel.isObservationGame(
66        ):
67            log.debug("ICPlayer.__onOfferAdd: emitting offer: self.gameno=%s self.name=%s %s" % (
68                self.gameno, self.name, offer))
69            self.offers[offer.index] = offer
70            self.emit("offer", offer)
71
72    def __onOfferDeclined(self, om, offer):
73        for offer_ in list(self.gamemodel.offers.keys()):
74            if offer.type == offer_.type:
75                offer.param = offer_.param
76        log.debug("ICPlayer.__onOfferDeclined: emitting decline for %s" %
77                  offer)
78        self.emit("decline", offer)
79
80    def __onOfferRemove(self, om, offer):
81        if offer.index in self.offers:
82            log.debug("ICPlayer.__onOfferRemove: emitting withdraw: \
83                      self.gameno=%s self.name=%s %s" % (self.gameno, self.name, offer))
84            # self.emit("withdraw", self.offers[offer.index])
85            del self.offers[offer.index]
86
87    def __onPrivateMessage(self, cm, name, title, isadmin, text):
88        if name == self.ichandle:
89            self.emit("offer", Offer(CHAT_ACTION, param=text))
90
91    def __disconnect(self):
92        log.debug("ICPlayer.__disconnect: %s" % self.name)
93        if self.connections is None:
94            return
95        for obj in self.connections:
96            for handler_id in self.connections[obj]:
97                if obj.handler_is_connected(handler_id):
98                    obj.disconnect(handler_id)
99        self.connections = None
100
101    def end(self, status=None, reason=None):
102        log.debug("ICPlayer.end: %s" % self.name)
103        self.__disconnect()
104        self.move_queue.put_nowait("end")
105
106    def kill(self, reason):
107        self.__disconnect()
108        self.move_queue.put_nowait("del")
109
110    # Send the player move updates
111
112    @asyncio.coroutine
113    def makeMove(self, board1, move, board2):
114        log.debug("ICPlayer.makemove: id(self)=%d self=%s move=%s board1=%s board2=%s" % (
115            id(self), self, move, board1, board2))
116        if board2 and not self.gamemodel.isObservationGame():
117            # TODO: Will this work if we just always use CASTLE_SAN?
118            castle_notation = CASTLE_KK
119            if board2.variant == FISCHERRANDOMCHESS:
120                castle_notation = CASTLE_SAN
121            self.connection.bm.sendMove(toAN(board2, move, castleNotation=castle_notation))
122            # wait for fics to send back our move we made
123            item = yield from self.move_queue.get()
124            log.debug("ICPlayer.makeMove: fics sent back the move we made")
125
126        item = yield from self.move_queue.get()
127        try:
128            if item == "end":
129                log.debug("ICPlayer.makeMove got: end")
130                raise GameEnded
131            elif item == "del":
132                log.debug("ICPlayer.makeMove got: del")
133                raise PlayerIsDead
134            elif item == "stm":
135                log.debug("ICPlayer.makeMove got: stm")
136                self.turn_interrupt = False
137                raise TurnInterrupt
138            elif item == "fen":
139                log.debug("ICPlayer.makeMove got: fen")
140                self.turn_interrupt = False
141                raise TurnInterrupt
142            elif item == "pass":
143                log.debug("ICPlayer.makeMove got: pass")
144                self.pass_interrupt = False
145                raise PassInterrupt
146
147            gameno, ply, curcol, lastmove, fen, wname, bname, wms, bms = item
148            log.debug("ICPlayer.makeMove got: %s %s %s %s" % (gameno, ply, curcol, lastmove))
149            self.gamemodel.update_board(gameno, ply, curcol, lastmove, fen, wname, bname, wms, bms)
150
151            if self.turn_interrupt:
152                self.turn_interrupt = False
153                raise TurnInterrupt
154
155            if self.pass_interrupt:
156                self.pass_interrupt = False
157                raise PassInterrupt
158
159            if ply < board1.ply:
160                # This should only happen in an observed game
161                board1 = self.gamemodel.getBoardAtPly(max(ply - 1, 0))
162            log.debug("ICPlayer.makemove: id(self)=%d self=%s from queue got: ply=%d sanmove=%s" % (
163                id(self), self, ply, lastmove))
164
165            try:
166                move = parseSAN(board1, lastmove)
167                log.debug("ICPlayer.makemove: id(self)=%d self=%s parsed move=%s" % (
168                    id(self), self, move))
169            except ParsingError:
170                raise
171            return move
172        finally:
173            log.debug("ICPlayer.makemove: id(self)=%d self=%s returning move=%s" % (id(self), self, move))
174
175    # Interacting with the player
176
177    def pause(self):
178        pass
179
180    def resume(self):
181        pass
182
183    def setBoard(self, fen):
184        # setBoard will currently only be called for ServerPlayer when starting
185        # to observe some game. In this case FICS already knows how the board
186        # should look, and we don't need to set anything
187        pass
188
189    def playerUndoMoves(self, movecount, gamemodel):
190        log.debug("ICPlayer.playerUndoMoves: id(self)=%d self=%s, undoing movecount=%d" % (
191            id(self), self, movecount))
192        # If current player has changed so that it is no longer us to move,
193        # We raise TurnInterrupt in order to let GameModel continue the game
194        if movecount % 2 == 1 and gamemodel.curplayer != self:
195            log.debug("ICPlayer.playerUndoMoves: set self.turn_interrupt = True %s" % self.name)
196            if self.connection.ICC:
197                self.move_queue.put_nowait("stm")
198            else:
199                self.turn_interrupt = True
200        if movecount % 2 == 0 and gamemodel.curplayer == self:
201            log.debug("ICPlayer.playerUndoMoves: set self.pass_interrupt = True %s" % self.name)
202            if self.connection.ICC:
203                self.move_queue.put_nowait("pass")
204            else:
205                self.pass_interrupt = True
206
207    def resetPosition(self):
208        """ Used in observed examined games f.e. when LectureBot starts another example"""
209        self.turn_interrupt = True
210
211    def putMessage(self, text):
212        self.connection.cm.tellPlayer(self.ichandle, text)
213
214    # Offer handling
215
216    def offerRematch(self):
217        if self.gamemodel.timed:
218            minimum = int(self.gamemodel.timemodel.intervals[0][0]) / 60
219            inc = self.gamemodel.timemodel.gain
220        else:
221            minimum = 0
222            inc = 0
223        self.connection.om.challenge(self.ichandle,
224                                     self.gamemodel.ficsgame.game_type, minimum,
225                                     inc, self.gamemodel.ficsgame.rated)
226
227    def offer(self, offer):
228        log.debug("ICPlayer.offer: self=%s %s" % (repr(self), offer))
229        if offer.type == TAKEBACK_OFFER:
230            # only 1 outstanding takeback offer allowed on FICS, so remove any of ours
231            for index in list(self.offers.keys()):
232                if self.offers[index].type == TAKEBACK_OFFER:
233                    log.debug("ICPlayer.offer: del self.offers[%s] %s" % (index, offer))
234                    del self.offers[index]
235        self.connection.om.offer(offer)
236
237    def offerDeclined(self, offer):
238        log.debug("ICPlayer.offerDeclined: sending decline for %s" % offer)
239        self.connection.om.decline(offer)
240
241    def offerWithdrawn(self, offer):
242        pass
243
244    def offerError(self, offer, error):
245        pass
246
247    def observe(self):
248        self.connection.client.run_command("observe %s" % self.ichandle)
249