Change Controller <-> History <-> MoveList interactions.
[kaya.git] / lib / controller.rb
blobec44ee1112d647e79a6e6ef31880258ee55884d9
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 :match
12   attr_reader :color
13   attr_reader :controlled
14   attr_reader :table
15   attr_reader :policy
16   attr_accessor :name
17   
18   def initialize(table)
19     @table = table
20     @scene = @table.scene
22     @pools = { }
23     @clocks = { }
24     
25     @field = AnimationField.new(20)
26   end
27   
28   def each_element
29     yield @board if @board
30     @pools.each {|c, pool| yield pool }
31     @clocks.each {|c, clock| yield clock }
32   end
33   
34   def on_board_click(p)
35     state = @match.history.state
36     if @board.selection
37       move = @policy.new_move(state, @board.selection, p)
38       validate = @match.game.validator.new(state)
39       if validate[move]
40         perform! move
41       end
42       
43       @board.selection = nil
44     elsif @policy.movable?(state, p) and movable?(p)
45       @board.selection = p
46     end
47   end
48   
49   def reset(match)
50     @match = 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     
59     @animator = @match.game.animator.new(@board)
60     @board.reset(match.state.board)
61     update_pools
62     
63     @clocks.each do |col, clock|
64       clock.stop
65     end
66     
67     @board.observe(:click) {|p| on_board_click(p) }
68     @board.observe(:drag) {|data| on_board_drag(data) }
69     @board.observe(:drop) {|data| on_board_drop(data) }
70     @pools.each do |col, pool|
71       pool.observe(:drag) {|data| on_pool_drag(col, data) }
72       pool.observe(:drop) {|data| on_pool_drop(col, data) }
73     end
74     @clocks.each do |col, clock|
75       clock.data = { :color => col,
76                      :player => match.player(col).name }
77     end
78     
79     @match.history.observe(:current_changed) { refresh }
80     
81     @match.observe(:move) do |data|
82       refresh(data[:opts])
83       @clocks[data[:old_state].turn].stop
84       @clocks[data[:state].turn].start
85     end
86     
87     @clocks[@match.game.players.first].active = true
88     @table.flip(@color && (@color != @match.game.players.first))
89     
90     if @match.history.move
91       @board.highlight(@match.history.move)
92     end
93   end
94   
95   def perform!(move, opts = {})
96     turn = @match.history.state.turn
97     if @controlled[turn]
98       @next_refresh_opts = opts
99       @match.move(@controlled[turn], move, opts)
100     end
101   end
102   
103   def back
104     @match.history.back
105   rescue History::OutOfBound
106     puts "error: first move"
107   end
108   
109   def forward
110     @match.history.forward
111   rescue History::OutOfBound
112     puts "error: last move"
113   end
114   
115   # sync displayed state with current history item
116   # 
117   def refresh(opts = { })
118     if @match
119       index = @match.history.current
120       if 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   end
133   
134   def go_to(index)
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     @field.run anim
143     
144     update_pools
145   end
146   
147   def update_pools
148     @pools.each do |col, pool|
149       anim = pool.animator.warp(@match.history.state.pool(col))
150       @field.run anim
151     end
152   end
153   
154   def on_board_drop(data)
155     if data[:src]
156       move = nil
157       
158       if data[:src] == data[:dst]
159         @board.selection = data[:src]
160       elsif data[:dst]
161         # normal move
162         move = @policy.new_move(
163           @match.history.state, data[:src], data[:dst])
164         validate = @match.game.validator.new(@match.history.state)
165         validate[move]
166       end
167       
168       if move and move.valid?
169         @board.add_to_group data[:item]
170         @board.lower data[:item]
171         perform! move, :adjust => true
172       else
173         cancel_drop(data)
174       end
175     elsif data[:index] and data[:dst]
176       # actual drop
177       move = @policy.new_move(
178         @match.history.state, nil, data[:dst], 
179         :dropped => data[:item].name)
180       validate = @match.game.validator.new(@match.history.state)
181       if validate[move]
182         @board.add_to_group data[:item]
183         @board.lower data[:item]
184         perform! move, :adjust => true, :dropped => data[:item]
185       else
186         cancel_drop(data)
187       end
188     end
189   end
190   
191   def on_board_drag(data)
192     if @policy.movable?(@match.history.state, data[:src]) and 
193        movable?(data[:src])
194       @board.raise data[:item]
195       @board.remove_from_group data[:item]
196       @board.selection = nil
197       @scene.on_drag(data)
198     end
199   end
200   
201   def on_pool_drag(c, data)
202     if @policy.droppable?(@match.history.state, c, data[:index]) and 
203        droppable?(c, data[:index])
204        
205        
206       # replace item with a correctly sized one
207       item = @board.create_piece(data[:item].name)
208       @board.raise item
209       @board.remove_from_group item
210       anim = @pools[c].animator.remove_piece(data[:index])
211       data[:item] = item
212       data[:size] = @board.unit
213       data[:pool_color] = c
214       
215       @scene.on_drag(data)
216       
217       @field.run anim
218     end
219   end
220   
221   def on_pool_drop(color, data)
222     cancel_drop(data)
223   end
224   
225   def cancel_drop(data)
226     anim = if data[:index]
227       # remove dragged item
228       data[:item].remove
229       # make original item reappear in its place
230       @pools[data[:pool_color]].animator.insert_piece(
231         data[:index],
232         data[:item].name)
233     elsif data[:src]
234       @board.add_to_group data[:item]
235       @board.lower data[:item]
236       @animator.movement(data[:item], nil, data[:src], Path::Linear)
237     end
238     
239     @field.run(anim) if anim
240   end
241   
242   def on_time(time)
243     time.each do |pl, seconds|
244       @clocks[pl].clock ||= Clock.new(seconds, 0, nil)
245       @clocks[pl].clock.set_time(seconds)
246     end
247   end
248   
249   def on_close(data)
250     @clocks.each do |pl, clock|
251       clock.stop
252     end
253     @controlled = { }
254   end
255   
256   def add_controlled_player(player)
257     @controlled[player.color] = player
258   end
259   
260   def color=(value)
261     @match.close if @match
262     @match = nil
263     
264     @color = value
265     if @color
266       @controlled = { @color => self }
267     end
268   end
269     
270   def movable?(p)
271     can_play?
272   end
273   
274   def droppable?(color, index)
275     can_play?
276   end
277   
278   private
279   
280   def can_play?
281     return false unless @controlled[@match.history.state.turn]
282     if @match.history.current < @match.index
283       @match.editable?
284     else
285       true
286     end
287   end