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