Parse castling in verbose notation.
[kaya.git] / lib / plugins / ics / lib / icsplayer.rb
blob4439e65cc8ba85d0cc1477905f408e0fbdfead7c
1 # Copyright (c) 2009 Paolo Capriotti <p.capriotti@gmail.com>
2
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 require 'interaction/match'
10 module ICS
12
13 # This class represents a remote player on ICS.
14
15 class ICSPlayer
16   include Player
17   include Observer
18   
19   attr_reader :color, :name
20   
21   #
22   # Create a new ICS player playing with the given color and using the given
23   # output channel to send moves.
24   # 
25   # The out parameter is a Proc that will be used to send data to the server.
26   # 
27   def initialize(out, color, match, match_info)
28     @color = color
29     @out = out
30     @match = match
31     @serializer = match.game.serializer.new(:simple)
32     @match_info = match_info
33     @name = match_info[color][:name]
34     @expected_navigations = []
35   end
37   # 
38   # Send a move to the server.
39   # 
40   def on_move(data)
41     text = @serializer.serialize(data[:move], 
42                                  data[:old_state])
43     @out[text]
44   end
45   
46   # 
47   # Send the server a request to move backwards.
48   # 
49   def on_back(opts)
50     @out['back']
51     add_expected_navigation(opts)
52   end
53   
54   # 
55   # Send the server a request to move forward.
56   #   
57   def on_forward(opts)
58     @out['forward']
59     add_expected_navigation(opts)
60   end
61   
62   # 
63   # Send a navigation request to the server.
64   # 
65   def on_go_to(data)
66     delta = data[:index] - data[:old_index]
67     add_expected_navigation(data) unless delta == 0
68     if delta > 0
69       @out["forward #{delta}"]
70     elsif delta < 0
71       @out["back #{-delta}"]
72     end
73   end
74   
75   # 
76   # Use the takeback command to request an undo.
77   # 
78   def allow_undo?(player, manager)
79     # request undo
80     @out['takeback']
81     # disallow for now
82     manager.undo(self, nil)
83   end
84   
85   # 
86   # Process an incoming style12 event.
87   # 
88   def on_style12(style12)
89     @match.update_time(style12.time)
90     delta = style12.move_index - @match.index
91     
92     # get previously stored revert information
93     revert_to = @match_info[:about_to_revert_to]
94     @match_info.delete(:about_to_revert_to)
95     
96     if revert_to and revert_to != style12.move_index
97       warn "ICS inconsistency: style12 and revert message refer to" +
98        "different indexes (resp. #{style12.move_index} and #{revert_to}"
99     end
100     
101     # check expected navigations
102     exp = @expected_navigations.shift
103     if exp
104       if exp != style12.move_index || revert_to
105         # unexpected navigation, clear expected queue
106         @expected_navigations = []
107       else
108         if exp == 0 || 
109            @match_info[:icsapi].
110              same_state(style12.state, 
111                         @match.history[exp].state)
112           # we were expecting this, no need to take further action
113           return
114         end
115       end
116     end
117     
118     if delta == 1
119       # standard case: advancing forward by 1
120       move = @serializer.deserialize(style12.last_move_san, @match.state)
121       if move.nil?
122         # An invalid move can happen when the game used locally does not
123         # correspond to the actual played game.
124         # This is sometimes inevitable, since ICS does not send a header
125         # when beginning examination.
126         # In this case, force the move into the history, and be careful to
127         # use the SAN provided in the style12 event for rendering.
128         warn "Received invalid move from ICS: #{style12.last_move_san}"
129         move = @match_info[:icsapi].parse_last_move(style12.last_move, style12.state.turn)
130         @match.history.add_move(style12.state, move, :text => style12.last_move_san)
131       else
132         # Perform and store a new move.
133         @match.move(self, move)
134         unless @match_info[:icsapi].
135                  same_state(style12.state, 
136                             @match.state)
137           @match.history.state = style12.state.dup
138         end
139       end
140     elsif delta <= 0
141       move = if style12.move_index > 0
142         @serializer.deserialize(
143           style12.last_move_san, 
144           @match.history[style12.move_index - 1].state)
145       end
146       state = @match_info[:icsapi].
147         amend_state(@match.history[style12.move_index].state, 
148                     style12.state)
149       if @match.navigable? && (!revert_to)
150         @match.history.go_to(style12.move_index)
151         @match.history.set_item(state, move)
152       else
153         @match.history.remove_items_at(style12.move_index + 1)
154         @match.history.set_item(state, move)
155       end
156     else
157       move = @match_info[:icsapi].parse_last_move(style12.last_move, style12.state.turn)
158       if move
159         @match.history.add_move(style12.state, move, :text => style12.last_move_san)
160       else
161         warn "Invalid last move #{style12.last_move}"
162       end
163     end
164   end
165   
166   private
167   
168   def add_expected_navigation(opts = {})
169     @expected_navigations << @match.index unless opts[:awaiting_server] 
170   end