1import re
2
3from gi.repository import GObject
4
5from pychess.Utils.const import DRAW_OFFER, ABORT_OFFER, ADJOURN_OFFER, TAKEBACK_OFFER, \
6    PAUSE_OFFER, RESUME_OFFER, SWITCH_OFFER, RESIGNATION, FLAG_CALL, MATCH_OFFER, \
7    WHITE, ACTION_ERROR_SWITCH_UNDERWAY, ACTION_ERROR_CLOCK_NOT_STARTED, \
8    ACTION_ERROR_CLOCK_NOT_PAUSED, ACTION_ERROR_NONE_TO_ACCEPT, ACTION_ERROR_NONE_TO_WITHDRAW, \
9    ACTION_ERROR_NONE_TO_DECLINE, ACTION_ERROR_TOO_LARGE_UNDO, ACTION_ERROR_NOT_OUT_OF_TIME
10
11from pychess.Utils.Offer import Offer
12from pychess.System.Log import log
13from pychess.ic import GAME_TYPES, VariantGameType
14from pychess.ic.FICSObjects import FICSChallenge
15
16names = r"\w+(?:\([A-Z\*]+\))*"
17
18rated = "(rated|unrated)"
19colors = r"(?:\[(white|black)\])?"
20ratings = r"\(([0-9\ \-\+]{1,4}[E P]?)\)"
21loaded_from = r"(?: Loaded from (wild[/\w]*))?"
22adjourned = r"(?: (\(adjourned\)))?"
23
24matchreUntimed = re.compile(r"(\w+) %s %s ?(\w+) %s %s (untimed)\s*" %
25                            (ratings, colors, ratings, rated))
26matchre = re.compile(
27    r"(\w+) %s %s ?(\w+) %s %s (\w+) (\d+) (\d+)%s%s" %
28    (ratings, colors, ratings, rated, loaded_from, adjourned))
29
30# <pf> 39 w=GuestDVXV t=match p=GuestDVXV (----) [black] GuestNXMP (----) unrated blitz 2 12
31# <pf> 16 w=GuestDVXV t=match p=GuestDVXV (----) GuestNXMP (----) unrated wild 2 12 Loaded from wild/fr
32# <pf> 39 w=GuestDVXV t=match p=GuestDVXV (----) GuestNXMP (----) unrated blitz 2 12 (adjourned)
33# <pf> 45 w=GuestGYXR t=match p=GuestGYXR (----) Lobais (----) unrated losers 2 12
34# <pf> 45 w=GuestYDDR t=match p=GuestYDDR (----) mgatto (1358) unrated untimed
35# <pf> 71 w=joseph t=match p=joseph (1632) mgatto (1742) rated wild 5 1 Loaded from wild/fr (adjourned)
36# <pf> 59 w=antiseptic t=match p=antiseptic (1945) mgatto (1729) rated wild 6 1 Loaded from wild/4 (adjourned)
37#
38# Known offers: abort accept adjourn draw match pause unpause switch takeback
39#
40
41strToOfferType = {
42    "draw": DRAW_OFFER,
43    "abort": ABORT_OFFER,
44    "adjourn": ADJOURN_OFFER,
45    "takeback": TAKEBACK_OFFER,
46    "pause": PAUSE_OFFER,
47    "unpause": RESUME_OFFER,
48    "switch": SWITCH_OFFER,
49    "resign": RESIGNATION,
50    "flag": FLAG_CALL,
51    "match": MATCH_OFFER
52}
53
54offerTypeToStr = {}
55for k, v in strToOfferType.items():
56    offerTypeToStr[v] = k
57
58
59class OfferManager(GObject.GObject):
60
61    __gsignals__ = {
62        'onOfferAdd': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
63        'onOfferRemove': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
64        'onOfferDeclined': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
65        'onChallengeAdd': (GObject.SignalFlags.RUN_FIRST, None, (object, )),
66        'onChallengeRemove': (GObject.SignalFlags.RUN_FIRST, None, (int, )),
67        'onActionError': (GObject.SignalFlags.RUN_FIRST, None, (object, int)),
68    }
69
70    def __init__(self, connection):
71        GObject.GObject.__init__(self)
72
73        self.connection = connection
74
75        self.connection.expect_line(
76            self.onOfferAdd, r"<p(t|f)> (\d+) w=%s t=(\w+) p=(.+)" % names)
77        self.connection.expect_line(self.onOfferRemove, r"<pr> (\d+)")
78
79        for ficsstring, offer, error in (
80            ("You cannot switch sides once a game is underway.",
81             Offer(SWITCH_OFFER), ACTION_ERROR_SWITCH_UNDERWAY),
82            ("Opponent is not out of time.", Offer(FLAG_CALL),
83             ACTION_ERROR_NOT_OUT_OF_TIME), ("The clock is not ticking yet.",
84                                             Offer(PAUSE_OFFER),
85                                             ACTION_ERROR_CLOCK_NOT_STARTED),
86            ("The clock is not ticking.", Offer(FLAG_CALL),
87             ACTION_ERROR_CLOCK_NOT_STARTED), ("The clock is not paused.",
88                                               Offer(RESUME_OFFER),
89                                               ACTION_ERROR_CLOCK_NOT_PAUSED)):
90            self.connection.expect_line(
91                lambda match: self.emit("onActionError", offer, error),
92                ficsstring)
93
94        self.connection.expect_line(
95            self.notEnoughMovesToUndo,
96            r"There are (?:(no)|only (\d+) half) moves in your game\.")
97
98        self.connection.expect_line(self.noOffersToAccept,
99                                    "There are no ([^ ]+) offers to (accept).")
100
101        self.connection.expect_line(
102            self.onOfferDeclined,
103            r"\w+ declines the (draw|takeback|pause|unpause|abort|adjourn) request\.")
104
105        self.lastPly = 0
106        self.offers = {}
107
108        self.connection.client.run_command("iset pendinfo 1")
109
110    def onOfferDeclined(self, match):
111        log.debug("OfferManager.onOfferDeclined: match.string=%s" %
112                  match.string)
113        type = match.groups()[0]
114        offer = Offer(strToOfferType[type])
115        self.emit("onOfferDeclined", offer)
116
117    def noOffersToAccept(self, match):
118        offertype, request = match.groups()
119        if request == "accept":
120            error = ACTION_ERROR_NONE_TO_ACCEPT
121        elif request == "withdraw":
122            error = ACTION_ERROR_NONE_TO_WITHDRAW
123        elif request == "decline":
124            error = ACTION_ERROR_NONE_TO_DECLINE
125        offer = Offer(strToOfferType[offertype])
126        self.emit("onActionError", offer, error)
127
128    def notEnoughMovesToUndo(self, match):
129        ply = match.groups()[0] or match.groups()[1]
130        if ply == "no":
131            ply = 0
132        else:
133            ply = int(ply)
134        offer = Offer(TAKEBACK_OFFER, param=ply)
135        self.emit("onActionError", offer, ACTION_ERROR_TOO_LARGE_UNDO)
136
137    def onOfferAdd(self, match):
138        log.debug("OfferManager.onOfferAdd: match.string=%s" % match.string)
139
140        tofrom, index, offertype, parameters = match.groups()
141        index = int(index)
142
143        if tofrom == "t":
144            # ICGameModel keeps track of the offers we've sent ourselves, so we
145            # don't need this
146            return
147        if offertype not in strToOfferType:
148            log.warning("OfferManager.onOfferAdd: Declining unknown offer type: " +
149                        "offertype=%s parameters=%s index=%d" % (offertype, parameters, index))
150            self.connection.client.run_command("decline %d" % index)
151            return
152        offertype = strToOfferType[offertype]
153        if offertype == TAKEBACK_OFFER:
154            offer = Offer(offertype, param=int(parameters), index=index)
155        else:
156            offer = Offer(offertype, index=index)
157        self.offers[offer.index] = offer
158
159        if offer.type == MATCH_OFFER:
160            is_adjourned = False
161            if matchreUntimed.match(parameters) is not None:
162                fname, frating, col, tname, trating, rated, type = \
163                    matchreUntimed.match(parameters).groups()
164                mins = 0
165                incr = 0
166                gametype = GAME_TYPES["untimed"]
167            else:
168                fname, frating, col, tname, trating, rated, gametype, mins, \
169                    incr, wildtype, adjourned = matchre.match(parameters).groups()
170                if (wildtype and "adjourned" in wildtype) or \
171                        (adjourned and "adjourned" in adjourned):
172                    is_adjourned = True
173                if wildtype and "wild" in wildtype:
174                    gametype = wildtype
175
176                try:
177                    gametype = GAME_TYPES[gametype]
178                except KeyError:
179                    log.warning("OfferManager.onOfferAdd: auto-declining " +
180                                "unknown offer type: '%s'\n" % gametype)
181                    self.decline(offer)
182                    del self.offers[offer.index]
183                    return
184
185            player = self.connection.players.get(fname)
186            rating = frating.strip()
187            rating = int(rating) if rating.isdigit() else 0
188            if player.ratings[gametype.rating_type] != rating:
189                player.ratings[gametype.rating_type] = rating
190                player.emit("ratings_changed", gametype.rating_type, player)
191            rated = rated != "unrated"
192            challenge = FICSChallenge(index,
193                                      player,
194                                      int(mins),
195                                      int(incr),
196                                      rated,
197                                      col,
198                                      gametype,
199                                      adjourned=is_adjourned)
200            self.emit("onChallengeAdd", challenge)
201
202        else:
203            log.debug("OfferManager.onOfferAdd: emitting onOfferAdd: %s" %
204                      offer)
205            self.emit("onOfferAdd", offer)
206
207    def onOfferRemove(self, match):
208        log.debug("OfferManager.onOfferRemove: match.string=%s" % match.string)
209        index = int(match.groups()[0])
210        if index not in self.offers:
211            return
212        if self.offers[index].type == MATCH_OFFER:
213            self.emit("onChallengeRemove", index)
214        else:
215            self.emit("onOfferRemove", self.offers[index])
216        del self.offers[index]
217
218    ###
219
220    def challenge(self,
221                  player_name,
222                  game_type,
223                  startmin,
224                  incsec,
225                  rated,
226                  color=None):
227        log.debug("OfferManager.challenge: %s %s %s %s %s %s" %
228                  (player_name, game_type, startmin, incsec, rated, color))
229        rchar = rated and "r" or "u"
230        if color is not None:
231            cchar = color == WHITE and "w" or "b"
232        else:
233            cchar = ""
234        s = "match %s %d %d %s %s" % \
235            (player_name, startmin, incsec, rchar, cchar)
236        if isinstance(game_type, VariantGameType):
237            s += " " + game_type.seek_text
238        self.connection.client.run_command(s)
239
240    def offer(self, offer):
241        log.debug("OfferManager.offer: %s" % offer)
242        s = offerTypeToStr[offer.type]
243        if offer.type == TAKEBACK_OFFER:
244            s += " " + str(offer.param)
245        self.connection.client.run_command(s)
246
247    ###
248
249    def withdraw(self, offer):
250        log.debug("OfferManager.withdraw: %s" % offer)
251        self.connection.client.run_command("withdraw t %s" %
252                                           offerTypeToStr[offer.type])
253
254    def accept(self, offer):
255        log.debug("OfferManager.accept: %s" % offer)
256        if offer.index is not None:
257            self.acceptIndex(offer.index)
258        else:
259            self.connection.client.run_command("accept t %s" %
260                                               offerTypeToStr[offer.type])
261
262    def decline(self, offer):
263        log.debug("OfferManager.decline: %s" % offer)
264        if offer.index is not None:
265            self.declineIndex(offer.index)
266        else:
267            self.connection.client.run_command("decline t %s" %
268                                               offerTypeToStr[offer.type])
269
270    def acceptIndex(self, index):
271        log.debug("OfferManager.acceptIndex: index=%s" % index)
272        self.connection.client.run_command("accept %s" % index)
273
274    def declineIndex(self, index):
275        log.debug("OfferManager.declineIndex: index=%s" % index)
276        self.connection.client.run_command("decline %s" % index)
277
278    def playIndex(self, index):
279        log.debug("OfferManager.playIndex: index=%s" % index)
280        self.connection.client.run_command("play %s" % index)
281