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