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