Use milliseconds to update ICS clocks.
[kaya.git] / lib / controller.rb
blob3e981efa0356b9194cc948675583378854b71102
1 require 'observer_utils'
2 require 'history'
3 require 'board/pool_animator'
4 require 'clock'
5 require 'interaction/match'
7 class Controller
8   include Observer
9   include Player
10   
11   attr_reader :history
12   attr_reader :color
13   attr_reader :controlled
14   attr_reader :table
15   attr_accessor :name
16   
17   def initialize(table)
18     @table = table
19     @scene = @table.scene
21     @pools = { }
22     @clocks = { }
23     
24     @field = AnimationField.new(20)
25   end
26   
27   def each_element
28     yield @board if @board
29     @pools.each {|c, pool| yield pool }
30     @clocks.each {|c, clock| yield clock }
31   end
32   
33   def on_board_click(p)
34     state = @match.state
35     if @board.selection
36       move = @match.game.policy.new_move(state, @board.selection, p)
37       validate = @match.game.validator.new(state)
38       if validate[move]
39         perform! move
40       end
41       
42       @board.selection = nil
43     elsif @match.game.policy.movable?(state, p) and movable?(p)
44       @board.selection = p
45     end
46   end
47   
48   def reset(match)
49     @match = match
50     @table.reset(@match)
51     @board = @table.elements[:board]
52     @pools = @table.elements[:pools]
53     @clocks = @table.elements[:clocks]
54     
55     @animator = @match.game.animator.new(@board)
56     @board.reset(match.state.board)
57     update_pools
58     
59     @clocks.each do |col, clock|
60       clock.stop
61     end
62     
63     @board.observe(:click) {|p| on_board_click(p) }
64     @board.observe(:drag) {|data| on_board_drag(data) }
65     @board.observe(:drop) {|data| on_board_drop(data) }
66     @pools.each do |col, pool|
67       pool.observe(:drag) {|data| on_pool_drag(col, data) }
68       pool.observe(:drop) {|data| on_pool_drop(col, data) }
69     end
70     @clocks.each do |col, clock|
71       clock.data = { :color => col,
72                      :player => match.player(col).name }
73     end
74     @match.observe(:move) do |data|
75       unless @controlled[data[:player].color] == data[:player]
76         animate(:forward, data[:state], data[:move])
77         @board.highlight(data[:move])
78         @clocks[data[:old_state].turn].stop
79         @clocks[data[:state].turn].start
80       end
81     end
82     
83     @clocks[@match.game.players.first].active = true
84     @table.flip(@color != @match.game.players.first)
85   end
86   
87   def perform!(move, opts = {})
88     col = @match.state.turn
89     if @controlled[col] and @match.move(@controlled[col], move)
90       animate(:forward, @match.state, move, opts)
91       @board.highlight(move)
92       
93       @clocks[col].stop
94       @clocks[@match.state.turn].start
95     end
96   end
97   
98   def back
99     state, move = @match.history.back
100     animate(:back, state, move)
101     @board.highlight(@match.history.move)
102   rescue History::OutOfBound
103     puts "error: first move"
104   end
105   
106   def forward
107     state, move = @match.history.forward
108     animate(:forward, state, move)
109     @board.highlight(move)
110   rescue History::OutOfBound
111     puts "error: last move"
112   end
113   
114   def go_to(index)
115     cur = @match.history.current
116     state, move = @match.history.go_to(index)
117     if index > cur
118       (cur + 1..index).each do |i|
119         animate(:forward, @match.history[i].state, @match.history[i].move)
120       end
121       @board.highlight(move)
122     elsif index < cur
123       (cur).downto(index + 1).each do |i|
124         animate(:back, @match.history[i - 1].state, @match.history[i].move)
125       end
126       @board.highlight(@match.history.move)
127     end
128   rescue History::OutOfBound
129     puts "error: no such index #{index}"
130   end
131   
132   def animate(direction, state, move, opts = {})
133     anim = @animator.send(direction, state, move, opts)
134     @field.run anim
135     
136     update_pools
137   end
138   
139   def update_pools
140     @pools.each do |col, pool|
141       anim = pool.animator.warp(@match.state.pool(col))
142       @field.run anim
143     end
144   end
145   
146   def on_board_drop(data)
147     if data[:src]
148       move = nil
149       
150       if data[:src] == data[:dst]
151         @board.selection = data[:src]
152       elsif data[:dst]
153         # normal move
154         move = @match.game.policy.new_move(
155           @match.state, data[:src], data[:dst])
156         validate = @match.game.validator.new(@match.state)
157         validate[move]
158       end
159       
160       if move and move.valid?
161         @board.add_to_group data[:item]
162         @board.lower data[:item]
163         perform! move, :adjust => true
164       else
165         cancel_drop(data)
166       end
167     elsif data[:index] and data[:dst]
168       # actual drop
169       move = @match.game.policy.new_move(
170         @match.state, nil, data[:dst], 
171         :dropped => data[:item].name)
172       validate = @match.game.validator.new(@match.state)
173       if validate[move]
174         @board.add_to_group data[:item]
175         @board.lower data[:item]
176         perform! move, :dropped => data[:item]
177       else
178         cancel_drop(data)
179       end
180     end
181   end
182   
183   def on_board_drag(data)
184     if @match.game.policy.movable?(@match.state, data[:src]) and 
185        movable?(data[:src])
186       @board.raise data[:item]
187       @board.remove_from_group data[:item]
188       @board.selection = nil
189       @scene.on_drag(data)
190     end
191   end
192   
193   def on_pool_drag(c, data)
194     if @match.game.policy.droppable?(@match.state, c, data[:index]) and 
195        droppable?(c, data[:index])
196        
197        
198       # replace item with a correctly sized one
199       item = @board.create_piece(data[:item].name)
200       @board.raise item
201       @board.remove_from_group item
202       anim = @pools[c].animator.remove_piece(data[:index])
203       data[:item] = item
204       data[:size] = @board.unit
205       data[:pool_color] = c
206       
207       @scene.on_drag(data)
208       
209       @field.run anim
210     end
211   end
212   
213   def on_pool_drop(color, data)
214     cancel_drop(data)
215   end
216   
217   def cancel_drop(data)
218     anim = if data[:index]
219       # remove dragged item
220       data[:item].remove
221       # make original item reappear in its place
222       @pools[data[:pool_color]].animator.insert_piece(
223         data[:index],
224         data[:item].name)
225     elsif data[:src]
226       @board.add_to_group data[:item]
227       @board.lower data[:item]
228       @animator.movement(data[:item], nil, data[:src], Path::Linear)
229     end
230     
231     @field.run(anim) if anim
232   end
233   
234   def on_time(time)
235     time.each do |pl, seconds|
236       @clocks[pl].clock ||= Clock.new(seconds, 0, nil)
237       @clocks[pl].clock.set_time(seconds)
238     end
239   end
240   
241   def add_controlled_player(player)
242     @controlled[player.color] = player
243   end
244   
245   def color=(value)
246     @color = value
247     @controlled = { @color => self }
248   end
249     
250   def movable?(p)
251     can_play?
252   end
253   
254   def droppable?(color, index)
255     can_play?
256   end
257   
258   private
259   
260   def can_play?
261     return false unless @controlled[@match.state.turn]
262     if @match.history.current < @match.index
263       @match.editable?
264     else
265       true
266     end
267   end