Update connect/disconnect action states.
[kaya.git] / lib / interaction / match.rb
blob68292d82407e4ba7318c380755ebf06cae8a6eef
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 'observer_utils'
9 require 'interaction/history'
10 require 'interaction/undo_manager'
12 module Player
13   def name
14   end
16   def inspect
17     "<#{name}:#{self.class.name}>"
18   end
19   
20   def controls?(x)
21     x == self
22   end
23 end
25 class Match
26   include Observable
27   
28   GameNotStarted = Class.new(Exception)
29   
30   attr_reader :game
31   attr_reader :kind
32   attr_accessor :url
33   
34   def initialize(game, opts = {})
35     @game = game
36     @players = { } # player => ready
37     @history = nil
38     @kind = opts[:kind] || :local
39     @opts = opts
40     @closed = false
41     @info = { }
42   end
43   
44   def register(player)
45     return false if @history
46     return false if @players.has_key?(player)
47     return false if complete?
48     return false unless @game.players.include?(player.color)
49     
50     @players[player] = false
51     fire :complete if complete?
52     true
53   end
54   
55   def start(player)
56     return false if @history
57     return false unless complete?
58     return false unless @players[player] == false
59     
60     @players[player] = true
61     if @players.values.all?
62       state = @game.state.new
63       state.setup
64       @history = History.new(state)
65       fire :started
66     end
68     true
69   end
71   def navigate(player, direction)
72     navigate_internal(player, direction, [])
73   end
74   
75   def go_to(player, index)
76     navigate_internal(player, :go_to, [index], 
77           :index => index, 
78           :old_index => history.current)
79   end
80   
81   def move(player, move, opts = {})
82     cancel_undo
83     if @closed
84       warn "a closed match is still active"
85     end
86     return false unless @history
87     
88     # if the match is non-editable, jump to the last move
89     unless editable?
90       @history.go_to_last 
91     end
92     
93     # if player is nil, assume the current player is moving
94     if player == nil
95       player = current_player
96     else
97       return false unless is_playing?(player)
98     end
100     validate = @game.validator.new(@history.state)
101     valid = validate[move]
102     unless valid
103       warn "Invalid move from #{player.name}: #{move}"
104       return false 
105     end
106     
107     old_state = @history.state
108     state = old_state.dup
109     state.perform! move
110     @history.add_move(state, move, opts)
111     
112     broadcast player, :move => {
113       :player => player,
114       :move => move,
115       :state => state,
116       :old_state => old_state }
117     true
118   end
119   
120   def undo!(player)
121     cancel_undo
122     @manager = UndoManager.new(@players.keys)
123     @manager.on(:execute) do |moves|
124       if moves
125         moves.times do
126           history.undo!
127         end
128       end
129       @manager = nil
130     end
131     @manager.undo(player, 1, :allow_more => true)
132     
133     # request permission from other players
134     @players.keys.each do |p|
135       p.allow_undo?(p, @manager) unless p == player
136     end
137   end
138   
139   def redo!(player)
140     cancel_undo
141     history.redo!
142     true
143   end
144   
145   def update_time(time)
146     broadcast nil, :time => time
147   end
148   
149   def complete?
150     @game.players.all? do |c| 
151       @players.keys.find {|p| p.color == c }
152     end
153   end
154   
155   def started?
156     @history
157   end
158   
159   def closed?
160     @closed
161   end
162   
163   def state
164     @history[index].state
165   end
166   
167   def editable?
168     @opts.fetch(:editable, true)
169   end
170   
171   def navigable?
172     @opts.fetch(:navigable, false)
173   end
174   
175   def time_running?
176     @opts.fetch(:time_running, false) and
177         index > 1
178   end
179   
180   def valid_state?
181     @history.valid_index?(index)
182   end
183     
184   def player(color)
185     @players.keys.find{|p| p.color == color }
186   end
187   
188   def current_player
189     player(state.turn)
190   end
191   
192   # end the match
193   # players must not send any more 'move' events to
194   # a closed game
195   # 
196   def close(result = nil, message = nil)
197     cancel_undo
198     @info[:result] = result if result
199     broadcast nil, :close => { 
200       :result => result,
201       :message => message }
202     @closed = true
203   end
204   
205   def info
206     @info.merge(:players => @players.keys)
207   end
208   
209   def add_info(infos)
210     @info = @info.merge(infos)
211     infos[:players].each do |col, name|
212       p = player(col)
213       if p
214         p.name = name
215       end
216     end
217   end
219   def index
220     if navigable?
221       @history.current
222     else
223       @history.size - 1
224     end
225   end
226   
227   def history
228     @history or raise(GameNotStarted)
229   end
230   
231   def history=(history)
232     @history = history
233   end
234     
235   private
236   
237   def broadcast(player, event)
238     return if @closed
239     fire event
240     @players.each_key do |p|
241       p.update any_to_event(event) unless p == player
242     end
243   end
244   
245   def cancel_undo
246     @manager.cancel if @manager
247     @manager = nil
248   end
249   
250   def navigate_internal(player, method, history_args, event_args = {})
251     return if @closed
252     awaiting_server = false
253     begin
254       history.send(method, *history_args)
255     rescue History::OutOfBound => e
256       # if navigable, the history may be missing
257       # items, they will be added later
258       raise e unless navigable?
259       awaiting_server = true
260     end
261     if navigable?
262       broadcast player, 
263         method => event_args.merge(:awaiting_server => awaiting_server)
264     end
265   end
266   
267   def is_playing?(player)
268     @players.each_key do |p|
269       return true if player.controls?(p)
270     end
271     false
272   end