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
30 def initialize(table, field)
41 yield @board if @board
42 @pools.each {|c, pool| yield pool }
43 @clocks.each {|c, clock| yield clock }
48 @policy = match.game.policy.new
49 @current = match.history.current
52 @board = @table.elements[:board]
53 @pools = @table.elements[:pools]
54 @clocks = @table.elements[:clocks]
55 @premover = Premover.new(self, @board, @pools)
57 @animator = match.game.animator.new(@board)
58 @board.reset(match.state.board)
61 @clocks.each do |col, clock|
65 @board.observe(:click) {|p| on_board_click(p) }
66 @board.observe(:drag) {|data| on_board_drag(data) }
67 @board.observe(:drop) {|data| on_board_drop(data) }
68 @pools.each do |col, pool|
69 pool.observe(:drag) {|data| on_pool_drag(col, data) }
70 pool.observe(:drop) {|data| on_pool_drop(col, data) }
72 @clocks.each do |col, clock|
73 clock.data = { :color => col,
74 :player => match.player(col).name }
77 match.history.observe(:current_changed) { refresh }
78 match.history.observe(:truncate) { refresh :instant => true }
79 match.history.observe(:force_update) { refresh :force => true }
80 match.history.observe(:new_move) do |data|
82 if match.time_running?
83 @clocks.each do |player, clock|
84 if data[:state].turn == player
93 @clocks[match.game.players.first].active = true
94 @table.flip(@color && (@color != match.game.players.first))
97 @board.highlight(match.history.move)
99 fire_active_actions(@current)
117 return unless match.editable?
121 # sync displayed state with current history item
122 # opts[:force] => update even if index == @current
123 # opts[:instant] => update without animating
125 def refresh(opts = { })
127 index = match.history.current
128 fire_active_actions(index)
129 return if index == @current && (!opts[:force])
130 if opts[:instant] || (index == @current && opts[:force])
131 anim = @animator.warp(match.history.state, opts)
132 perform_animation anim
133 elsif index > @current
134 (@current + 1..index).each do |i|
135 animate(:forward, match.history[i].state, match.history[i].move, opts)
137 elsif index < @current
138 @current.downto(index + 1).each do |i|
139 animate(:back, match.history[i - 1].state, match.history[i].move, opts)
143 @board.highlight(match.history[@current].move)
144 if @premover.index and @current == @premover.index + 1
151 def fire_active_actions(index)
152 fire :active_actions => {
153 :forward => match.navigable? || index < match.history.size - 1,
155 :undo => @color && match.history.operations.current >= 0,
156 :redo => @color && match.editable? && match.history.operations.current < match.history.operations.size - 1,
162 match.history.go_to(index)
163 rescue History::OutOfBound
164 puts "error: no such index #{index}"
167 def animate(direction, state, move, opts = {})
168 anim = @animator.send(direction, state, move, opts)
169 perform_animation anim
172 def perform_animation(anim)
177 def on_board_click(p)
179 state = match.history.state
180 # if there is a selection already, move or premove
181 # to the clicked square
183 case policy.movable?(match.history.state, @board.selection)
186 execute_move(@board.selection, p)
188 # schedule a premove on the board
189 @premover.move(@current, @board.selection, p)
191 @board.selection = nil
192 elsif movable?(state, p)
198 def on_board_drop(data)
201 @board.add_to_group data[:item]
202 @board.lower data[:item]
205 # board to board drop
206 if data[:src] == data[:dst]
207 # null drop, handle as a click
208 @board.selection = data[:src]
210 # normal move/premove
211 case policy.movable?(match.history.state, data[:src])
213 move = execute_move(data[:src], data[:dst], :adjust => true)
215 @premover.move(@current, data[:src], data[:dst])
218 elsif data[:index] and data[:dst]
220 case droppable?(match.history.state,
224 move = execute_drop(data[:item], data[:dst])
226 @premover.drop(@current, data[:pool_color], data[:index], data[:dst])
230 cancel_drop(data) unless move
233 def on_board_drag(data)
235 if movable?(match.history.state, data[:src])
236 @board.raise data[:item]
237 @board.remove_from_group data[:item]
238 data[:item].parent_item = nil
239 @board.selection = nil
244 def on_pool_drag(c, data)
246 if droppable?(match.history.state, c, data[:index])
247 # replace item with a correctly sized one
248 item = @board.create_piece(data[:item].name)
250 @board.remove_from_group item
251 item.parent_item = nil
252 anim = @pools[c].animator.remove_piece(data[:index])
254 data[:size] = @board.unit
255 data[:pool_color] = c
263 def on_pool_drop(color, data)
268 time.each do |pl, seconds|
269 @clocks[pl].clock ||= Clock.new(seconds, 0, nil)
270 @clocks[pl].clock.set_time(seconds)
275 @clocks.each do |pl, clock|
281 def add_controlled_player(player)
282 @controlled[player.color] = player
286 @match.close if @match
291 @controlled = { @color => self }
298 if match && match.editable?
299 manager.undo(1, :allow_more => true)
307 def navigate(direction)
309 match.navigate(self, direction)
310 rescue History::OutOfBound
311 puts "error: out of bound"
314 def movable?(state, p)
315 result = policy.movable?(state, p)
316 return false unless result
317 return false unless result == :movable || @premove
318 return false unless @controlled[state.board[p].color]
319 return false if match.history.current < match.index and (not match.editable?)
323 def droppable?(state, color, index)
324 result = policy.droppable?(state, color, index)
325 return false unless result
326 return false unless result == :droppable || @premove
327 return false unless @controlled[color]
328 return false if match.history.current < match.index and (not match.editable?)
332 def perform!(move, opts = {})
333 turn = match.history.state.turn
334 match.move(self, move, opts)
337 def cancel_drop(data)
338 anim = if data[:index]
339 # remove dragged item
341 # make original item reappear in its place
342 @pools[data[:pool_color]].animator.insert_piece(
346 @animator.movement(data[:item], nil, data[:src], Path::Linear)
349 @field.run(anim) if anim
353 @pools.each do |col, pool|
354 anim = pool.animator.warp(match.history.state.pool(col))