Update connect/disconnect action states.
[kaya.git] / lib / controller.rb
blob3c13e530180b64160575e298f5df2e4e6c4f370f
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 'board/pool_animator'
11 require 'clock'
12 require 'interaction/match'
13 require 'premover'
14 require 'executor'
16 class Controller
17   include Observer
18   include Observable
19   include Player
20   include Executor
21   
22   attr_reader :match, :policy
23   attr_reader :color
24   attr_reader :controlled
25   attr_reader :table
26   attr_reader :policy
27   attr_reader :dirty
28   attr_accessor :name
29   attr_accessor :premove
30   attr_reader :active_actions
31   
32   def initialize(table, field)
33     @table = table
34     @scene = @table.scene
36     @pools = { }
37     @clocks = { }
38     @field = field
39     @controlled = { }
40     @active_actions = Hash.new(false)
41   end
42   
43   def each_element
44     yield @board if @board
45     @pools.each {|c, pool| yield pool }
46     @clocks.each {|c, clock| yield clock }
47   end
48   
49   def reset(match = nil)
50     if @match and match
51       @match.close
52     end
53     match ||= @match
54     
55     @match = match if match
56     @policy = match.game.policy.new
57     @current = match.history.current
58     
59     @table.reset(match)
60     @board = @table.elements[:board]
61     @pools = @table.elements[:pools]
62     @clocks = @table.elements[:clocks]
63     @premover = Premover.new(self, @board, @pools)
64     
65     @animator = match.game.animator.new(@board)
66     @board.reset(match.state.board)
67     update_pools
68     
69     @clocks.each do |col, clock|
70       clock.stop
71     end
72     
73     @board.on(:click) {|p| on_board_click(p) }
74     @board.on(:drag) {|data| on_board_drag(data) }
75     @board.on(:drop) {|data| on_board_drop(data) }
76     @pools.each do |col, pool|
77       pool.on(:drag) {|data| on_pool_drag(col, data) }
78       pool.on(:drop) {|data| on_pool_drop(col, data) }
79     end
80     @clocks.each do |col, clock|
81       clock.data = { :color => col,
82                      :player => match.player(col).name }
83     end
84     
85     match.history.on(:current_changed) { refresh }
86     match.history.on(:truncate) { refresh :instant => true }
87     match.history.on(:force_update) { refresh :force => true }
88     match.history.on(:new_move) do |data|
89       refresh(data[:opts])
90     end
91     
92     @clocks[match.game.players.first].active = true
93     @table.flip(@color && (@color != match.game.players.first))
94     
95     if match.history.move
96       @board.highlight(match.history.move)
97     end
98     set_active_actions(@current)
99     fire :reset
100   end
101   
102   def back
103     navigate :back
104   end
105   
106   def forward
107     navigate :forward
108   end
109   
110   def undo!
111     return unless match
112     match.undo!(self)
113   end
114   
115   def redo!
116     return unless match
117     return unless match.editable?
118     match.redo!(self)
119   end
120   
121   # sync displayed state with current history item
122   # opts[:force] => update even if index == @current
123   # opts[:instant] => update without animating
124   # 
125   def refresh(opts = { })
126     return unless match
127     return unless match.valid_state?
128     
129     index = match.history.current
130     return if index == @current && (!opts[:force])
131     set_active_actions(index)
132     
133     fire :activity
134     
135     # update active clock
136     @clocks.each do |color, clock|
137       clock.active = match.history.state.turn == color
138     end
139     
140     # update running clock
141     if match.time_running?
142       @clocks.each do |player, clock|
143         if match.history.state.turn == player
144           clock.start
145         else
146           clock.stop
147         end
148       end
149     end
150     
151     if opts[:instant] || 
152        (index == @current && opts[:force]) ||
153        (not match.history.path_defined?(@current, index))
154       anim = @animator.warp(match.history.state, opts)
155       perform_animation anim
156     elsif index > @current
157       (@current + 1..index).each do |i|
158         animate(:forward, match.history[i].state, match.history[i].move, opts)
159       end
160     elsif index < @current
161       @current.downto(index + 1).each do |i|
162         animate(:back, match.history[i - 1].state, match.history[i].move, opts)
163       end
164     end
165     @current = index
166     @board.highlight(match.history[@current].move)
167     if @premover.index and @current == @premover.index + 1
168       @premover.execute
169     else
170       @premover.cancel
171     end
172   end
173   
174   def set_active_actions(index)
175     @active_actions = {
176       :forward => match.navigable? || index < match.history.size - 1,
177       :back => index > 0,
178       :undo => @color && match.history.operations.current >= 0,
179       :redo => @color && match.editable? && 
180                match.history.operations.current < match.history.operations.size - 1,
181     }
182     fire :changed_active_actions
183   end
184   
185   def go_to(index)
186     return unless match
187     match.history.go_to(index)
188   rescue History::OutOfBound
189     puts "error: no such index #{index}"
190   end
191   
192   def animate(direction, state, move, opts = {})
193     anim = @animator.send(direction, state, move, opts)
194     perform_animation anim
195   end
196   
197   def perform_animation(anim)
198     @field.run anim
199     update_pools
200   end
201   
202   def on_board_click(p)
203     return unless match
204     return unless match.started?
205     state = match.history.state
206     # if there is a selection already, move or premove
207     # to the clicked square
208     if @board.selection
209       case policy.movable?(match.history.state, @board.selection)
210       when :movable
211         # move directly
212         execute_move(@board.selection, p)
213       when :premovable
214         # schedule a premove on the board
215         @premover.move(@current, @board.selection, p)
216       end
217       @board.selection = nil
218     elsif movable?(state, p)
219       # only set selection
220       @board.selection = p
221     end
222   end
223   
224   def on_board_drop(data)
225     return unless match
226     move = nil
227     @board.add_to_group data[:item]
228     @board.lower data[:item]
229     
230     if data[:src]
231       # board to board drop
232       if data[:src] == data[:dst]
233         # null drop, handle as a click
234         @board.selection = data[:src]
235       elsif data[:dst]
236         # normal move/premove
237         case policy.movable?(match.history.state, data[:src])
238         when :movable
239           move = execute_move(data[:src], data[:dst], :adjust => true)
240         when :premovable
241           @premover.move(@current, data[:src], data[:dst])
242         end
243       end
244     elsif data[:index] and data[:dst]
245       # actual drop
246       case droppable?(match.history.state, 
247                       data[:pool_color], 
248                       data[:index])
249       when :droppable
250         move = execute_drop(data[:item], data[:dst])
251       when :predroppable
252         @premover.drop(@current, data[:pool_color], data[:index], data[:dst])
253       end
254     end
255     
256     cancel_drop(data) unless move
257   end
258   
259   def on_board_drag(data)
260     return unless match
261     if movable?(match.history.state, data[:src])
262       @board.raise data[:item]
263       @board.remove_from_group data[:item]
264       data[:item].parent_item = nil
265       @board.selection = nil
266       @scene.on_drag(data)
267     end
268   end
269   
270   def on_pool_drag(c, data)
271     return unless match
272     if droppable?(match.history.state, c, data[:index])
273       # replace item with a correctly sized one
274       item = @board.create_piece(data[:item].name)
275       @board.raise item
276       @board.remove_from_group item
277       item.parent_item = nil
278       anim = @pools[c].animator.remove_piece(data[:index])
279       data[:item] = item
280       data[:size] = @board.unit
281       data[:pool_color] = c
282       
283       @scene.on_drag(data)
284       
285       @field.run anim
286     end
287   end
288   
289   def on_pool_drop(color, data)
290     cancel_drop(data)
291   end
292     
293   def on_time(time)
294     time.each do |pl, ms|
295       if @clocks[pl].clock
296         @clocks[pl].clock.set_time(ms)
297       else
298         @clocks[pl].clock ||= Clock.new(ms, 0, Qt::Timer)
299       end
300     end
301   end
302   
303   def on_close(data)
304     @clocks.each do |pl, clock|
305       clock.stop
306     end
307     @controlled = { }
308   end
309   
310   def add_controlled_player(player)
311     @controlled[player.color] = player
312   end
313   
314   def controls?(x)
315     return true if x == self
316     @controlled.each do |c, p|
317       return true if x == p
318     end
319     false
320   end
321   
322   def color=(value)
323     @match.close if @match
324     @match = nil
325     
326     @color = value
327     if @color
328       @controlled = { @color => self }
329     else
330       @controlled = { }
331     end
332   end
333     
334   def allow_undo?
335     if match && match.editable?
336       manager.undo(1, :allow_more => true)
337     else
338       manager.undo(nil)
339     end
340   end
341     
342   def close
343     @match.close
344   end
345   
346   private
347   
348   def navigate(direction)
349     return unless match
350     match.navigate(self, direction)
351   rescue History::OutOfBound
352     puts "error: out of bound"
353   end
354   
355   def movable?(state, p)
356     result = policy.movable?(state, p)
357     return false unless result
358     return false unless result == :movable || @premove
359     return false unless @controlled[state.board[p].color]
360     return false if match.history.current < match.index and (not match.editable?)
361     result
362   end
363   
364   def droppable?(state, color, index)
365     result = policy.droppable?(state, color, index)
366     return false unless result
367     return false unless result == :droppable || @premove
368     return false unless @controlled[color]
369     return false if match.history.current < match.index and (not match.editable?)
370     result
371   end
372   
373   def perform!(move, opts = {})
374     turn = match.history.state.turn
375     match.move(self, move, opts)
376     fire :dirty unless @dirty
377     @dirty = true
378   end
379   
380   def cancel_drop(data)
381     anim = if data[:index]
382       # remove dragged item
383       data[:item].remove
384       # make original item reappear in its place
385       @pools[data[:pool_color]].animator.insert_piece(
386         data[:index],
387         data[:item].name)
388     elsif data[:src]
389           @board.add_to_group data[:item]
390           @board.lower data[:item]
391       @animator.movement(data[:item], nil, data[:src], Path::Linear)
392     end
393     
394     @field.run(anim) if anim
395   end
396   
397   def update_pools
398     @pools.each do |col, pool|
399       anim = pool.animator.warp(match.history.state.pool(col))
400       @field.run anim
401     end
402   end