1 # Copyright (c) 2009 Paolo Capriotti <p.capriotti@gmail.com>
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'
12 require 'interaction/match'
22 attr_reader :match, :policy
24 attr_reader :controlled
28 attr_accessor :premove
29 attr_reader :active_actions
31 def initialize(table, field)
39 @active_actions = Hash.new(false)
43 yield @board if @board
44 @pools.each {|c, pool| yield pool }
45 @clocks.each {|c, clock| yield clock }
48 def reset(match = nil)
50 @match = match if match
51 @policy = match.game.policy.new
52 @current = match.history.current
55 @board = @table.elements[:board]
56 @pools = @table.elements[:pools]
57 @clocks = @table.elements[:clocks]
58 @premover = Premover.new(self, @board, @pools)
60 @animator = match.game.animator.new(@board)
61 @board.reset(match.state.board)
64 @clocks.each do |col, clock|
68 @board.on(:click) {|p| on_board_click(p) }
69 @board.on(:drag) {|data| on_board_drag(data) }
70 @board.on(:drop) {|data| on_board_drop(data) }
71 @pools.each do |col, pool|
72 pool.on(:drag) {|data| on_pool_drag(col, data) }
73 pool.on(:drop) {|data| on_pool_drop(col, data) }
75 @clocks.each do |col, clock|
76 clock.data = { :color => col,
77 :player => match.player(col).name }
80 match.history.on(:current_changed) { refresh }
81 match.history.on(:truncate) { refresh :instant => true }
82 match.history.on(:force_update) { refresh :force => true }
83 match.history.on(:new_move) do |data|
85 if match.time_running?
86 @clocks.each do |player, clock|
87 if data[:state].turn == player
96 @clocks[match.game.players.first].active = true
97 @table.flip(@color && (@color != match.game.players.first))
100 @board.highlight(match.history.move)
102 set_active_actions(@current)
120 return unless match.editable?
124 # sync displayed state with current history item
125 # opts[:force] => update even if index == @current
126 # opts[:instant] => update without animating
128 def refresh(opts = { })
130 index = match.history.current
131 return if index == @current && (!opts[:force])
132 set_active_actions(index)
134 @clocks.each do |color, clock|
135 clock.active = match.history.state.turn == color
137 if opts[:instant] || (index == @current && opts[:force])
138 anim = @animator.warp(match.history.state, opts)
139 perform_animation anim
140 elsif index > @current
141 (@current + 1..index).each do |i|
142 animate(:forward, match.history[i].state, match.history[i].move, opts)
144 elsif index < @current
145 @current.downto(index + 1).each do |i|
146 animate(:back, match.history[i - 1].state, match.history[i].move, opts)
150 @board.highlight(match.history[@current].move)
151 if @premover.index and @current == @premover.index + 1
158 def set_active_actions(index)
160 :forward => match.navigable? || index < match.history.size - 1,
162 :undo => @color && match.history.operations.current >= 0,
163 :redo => @color && match.editable? &&
164 match.history.operations.current < match.history.operations.size - 1,
166 fire :changed_active_actions
171 match.history.go_to(index)
172 rescue History::OutOfBound
173 puts "error: no such index #{index}"
176 def animate(direction, state, move, opts = {})
177 anim = @animator.send(direction, state, move, opts)
178 perform_animation anim
181 def perform_animation(anim)
186 def on_board_click(p)
188 state = match.history.state
189 # if there is a selection already, move or premove
190 # to the clicked square
192 case policy.movable?(match.history.state, @board.selection)
195 execute_move(@board.selection, p)
197 # schedule a premove on the board
198 @premover.move(@current, @board.selection, p)
200 @board.selection = nil
201 elsif movable?(state, p)
207 def on_board_drop(data)
210 @board.add_to_group data[:item]
211 @board.lower data[:item]
214 # board to board drop
215 if data[:src] == data[:dst]
216 # null drop, handle as a click
217 @board.selection = data[:src]
219 # normal move/premove
220 case policy.movable?(match.history.state, data[:src])
222 move = execute_move(data[:src], data[:dst], :adjust => true)
224 @premover.move(@current, data[:src], data[:dst])
227 elsif data[:index] and data[:dst]
229 case droppable?(match.history.state,
233 move = execute_drop(data[:item], data[:dst])
235 @premover.drop(@current, data[:pool_color], data[:index], data[:dst])
239 cancel_drop(data) unless move
242 def on_board_drag(data)
244 if movable?(match.history.state, data[:src])
245 @board.raise data[:item]
246 @board.remove_from_group data[:item]
247 data[:item].parent_item = nil
248 @board.selection = nil
253 def on_pool_drag(c, data)
255 if droppable?(match.history.state, c, data[:index])
256 # replace item with a correctly sized one
257 item = @board.create_piece(data[:item].name)
259 @board.remove_from_group item
260 item.parent_item = nil
261 anim = @pools[c].animator.remove_piece(data[:index])
263 data[:size] = @board.unit
264 data[:pool_color] = c
272 def on_pool_drop(color, data)
277 time.each do |pl, seconds|
278 @clocks[pl].clock ||= Clock.new(seconds, 0, nil)
279 @clocks[pl].clock.set_time(seconds)
284 @clocks.each do |pl, clock|
290 def add_controlled_player(player)
291 @controlled[player.color] = player
295 @match.close if @match
300 @controlled = { @color => self }
307 if match && match.editable?
308 manager.undo(1, :allow_more => true)
316 def navigate(direction)
318 match.navigate(self, direction)
319 rescue History::OutOfBound
320 puts "error: out of bound"
323 def movable?(state, p)
324 result = policy.movable?(state, p)
325 return false unless result
326 return false unless result == :movable || @premove
327 return false unless @controlled[state.board[p].color]
328 return false if match.history.current < match.index and (not match.editable?)
332 def droppable?(state, color, index)
333 result = policy.droppable?(state, color, index)
334 return false unless result
335 return false unless result == :droppable || @premove
336 return false unless @controlled[color]
337 return false if match.history.current < match.index and (not match.editable?)
341 def perform!(move, opts = {})
342 turn = match.history.state.turn
343 match.move(self, move, opts)
346 def cancel_drop(data)
347 anim = if data[:index]
348 # remove dragged item
350 # make original item reappear in its place
351 @pools[data[:pool_color]].animator.insert_piece(
355 @board.add_to_group data[:item]
356 @board.lower data[:item]
357 @animator.movement(data[:item], nil, data[:src], Path::Linear)
360 @field.run(anim) if anim
364 @pools.each do |col, pool|
365 anim = pool.animator.warp(match.history.state.pool(col))