Add 'open' action.
[kaya.git] / lib / controller.rb
blob43c44d9ce13f8826009594dac996c5961651ff13
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_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     
86     if @match.history.move
87       @board.highlight(@match.history.move)
88     end
89   end
90   
91   def perform!(move, opts = {})
92     col = @match.state.turn
93     if @controlled[col] and @match.move(@controlled[col], move)
94       animate(:forward, @match.state, move, opts)
95       @board.highlight(move)
96       
97       @clocks[col].stop
98       @clocks[@match.state.turn].start
99     end
100   end
101   
102   def back
103     state, move = @match.history.back
104     animate(:back, state, move)
105     @board.highlight(@match.history.move)
106   rescue History::OutOfBound
107     puts "error: first move"
108   end
109   
110   def forward
111     state, move = @match.history.forward
112     animate(:forward, state, move)
113     @board.highlight(move)
114   rescue History::OutOfBound
115     puts "error: last move"
116   end
117   
118   def go_to(index)
119     cur = @match.history.current
120     state, move = @match.history.go_to(index)
121     if index > cur
122       (cur + 1..index).each do |i|
123         animate(:forward, @match.history[i].state, @match.history[i].move)
124       end
125       @board.highlight(move)
126     elsif index < cur
127       (cur).downto(index + 1).each do |i|
128         animate(:back, @match.history[i - 1].state, @match.history[i].move)
129       end
130       @board.highlight(@match.history.move)
131     end
132   rescue History::OutOfBound
133     puts "error: no such index #{index}"
134   end
135   
136   def animate(direction, state, move, opts = {})
137     anim = @animator.send(direction, state, move, opts)
138     @field.run anim
139     
140     update_pools
141   end
142   
143   def update_pools
144     @pools.each do |col, pool|
145       anim = pool.animator.warp(@match.state.pool(col))
146       @field.run anim
147     end
148   end
149   
150   def on_board_drop(data)
151     if data[:src]
152       move = nil
153       
154       if data[:src] == data[:dst]
155         @board.selection = data[:src]
156       elsif data[:dst]
157         # normal move
158         move = @match.game.policy.new_move(
159           @match.state, data[:src], data[:dst])
160         validate = @match.game.validator.new(@match.state)
161         validate[move]
162       end
163       
164       if move and move.valid?
165         @board.add_to_group data[:item]
166         @board.lower data[:item]
167         perform! move, :adjust => true
168       else
169         cancel_drop(data)
170       end
171     elsif data[:index] and data[:dst]
172       # actual drop
173       move = @match.game.policy.new_move(
174         @match.state, nil, data[:dst], 
175         :dropped => data[:item].name)
176       validate = @match.game.validator.new(@match.state)
177       if validate[move]
178         @board.add_to_group data[:item]
179         @board.lower data[:item]
180         perform! move, :dropped => data[:item]
181       else
182         cancel_drop(data)
183       end
184     end
185   end
186   
187   def on_board_drag(data)
188     if @match.game.policy.movable?(@match.state, data[:src]) and 
189        movable?(data[:src])
190       @board.raise data[:item]
191       @board.remove_from_group data[:item]
192       @board.selection = nil
193       @scene.on_drag(data)
194     end
195   end
196   
197   def on_pool_drag(c, data)
198     if @match.game.policy.droppable?(@match.state, c, data[:index]) and 
199        droppable?(c, data[:index])
200        
201        
202       # replace item with a correctly sized one
203       item = @board.create_piece(data[:item].name)
204       @board.raise item
205       @board.remove_from_group item
206       anim = @pools[c].animator.remove_piece(data[:index])
207       data[:item] = item
208       data[:size] = @board.unit
209       data[:pool_color] = c
210       
211       @scene.on_drag(data)
212       
213       @field.run anim
214     end
215   end
216   
217   def on_pool_drop(color, data)
218     cancel_drop(data)
219   end
220   
221   def cancel_drop(data)
222     anim = if data[:index]
223       # remove dragged item
224       data[:item].remove
225       # make original item reappear in its place
226       @pools[data[:pool_color]].animator.insert_piece(
227         data[:index],
228         data[:item].name)
229     elsif data[:src]
230       @board.add_to_group data[:item]
231       @board.lower data[:item]
232       @animator.movement(data[:item], nil, data[:src], Path::Linear)
233     end
234     
235     @field.run(anim) if anim
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     puts data[:message]
247     @clocks.each do |pl, clock|
248       clock.stop
249     end
250     @controlled = { }
251   end
252   
253   def add_controlled_player(player)
254     @controlled[player.color] = player
255   end
256   
257   def color=(value)
258     @color = value
259     @controlled = { @color => self }
260   end
261     
262   def movable?(p)
263     can_play?
264   end
265   
266   def droppable?(color, index)
267     can_play?
268   end
269   
270   private
271   
272   def can_play?
273     return false unless @controlled[@match.state.turn]
274     if @match.history.current < @match.index
275       @match.editable?
276     else
277       true
278     end
279   end