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
29 attr_accessor :premove
30 attr_reader :active_actions
32 def initialize(table, field)
40 @active_actions = Hash.new(false)
44 yield @board if @board
45 @pools.each {|c, pool| yield pool }
46 @clocks.each {|c, clock| yield clock }
49 def reset(match = nil)
55 @match = match if match
56 @policy = match.game.policy.new
57 @current = match.history.current
60 @board = @table.elements[:board]
61 @pools = @table.elements[:pools]
62 @clocks = @table.elements[:clocks]
63 @premover = Premover.new(self, @board, @pools)
65 @animator = match.game.animator.new(@board)
66 @board.reset(match.state.board)
69 @clocks.each do |col, clock|
73 @board.on(:click) {|p| on_board_click(p) }
74 @board.on(:drag) {|data| on_board_drag(data) }
75 @board.on(:drop) {|data| on_board_drop(data) }
76 @pools.each do |col, pool|
77 pool.on(:drag) {|data| on_pool_drag(col, data) }
78 pool.on(:drop) {|data| on_pool_drop(col, data) }
80 @clocks.each do |col, clock|
81 clock.data = { :color => col,
82 :player => match.player(col).name }
85 match.history.on(:current_changed) { refresh }
86 match.history.on(:truncate) { refresh :instant => true }
87 match.history.on(:force_update) { refresh :force => true }
88 match.history.on(:new_move) do |data|
92 @clocks[match.game.players.first].active = true
93 @table.flip(@color && (@color != match.game.players.first))
96 @board.highlight(match.history.move)
98 set_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 return unless match.valid_state?
129 index = match.history.current
130 return if index == @current && (!opts[:force])
131 set_active_actions(index)
135 # update active clock
136 @clocks.each do |color, clock|
137 clock.active = match.history.state.turn == color
140 # update running clock
141 if match.time_running?
142 @clocks.each do |player, clock|
143 if match.history.state.turn == player
152 (index == @current && opts[:force]) ||
153 (not match.history.path_defined?(@current, index))
154 anim = @animator.warp(match.history.state, opts)
155 perform_animation anim
156 elsif index > @current
157 (@current + 1..index).each do |i|
158 animate(:forward, match.history[i].state, match.history[i].move, opts)
160 elsif index < @current
161 @current.downto(index + 1).each do |i|
162 animate(:back, match.history[i - 1].state, match.history[i].move, opts)
166 @board.highlight(match.history[@current].move)
167 if @premover.index and @current == @premover.index + 1
174 def set_active_actions(index)
176 :forward => match.navigable? || index < match.history.size - 1,
178 :undo => @color && match.history.operations.current >= 0,
179 :redo => @color && match.editable? &&
180 match.history.operations.current < match.history.operations.size - 1,
182 fire :changed_active_actions
187 match.history.go_to(index)
188 rescue History::OutOfBound
189 puts "error: no such index #{index}"
192 def animate(direction, state, move, opts = {})
193 anim = @animator.send(direction, state, move, opts)
194 perform_animation anim
197 def perform_animation(anim)
202 def on_board_click(p)
204 return unless match.started?
205 state = match.history.state
206 # if there is a selection already, move or premove
207 # to the clicked square
209 case policy.movable?(match.history.state, @board.selection)
212 execute_move(@board.selection, p)
214 # schedule a premove on the board
215 @premover.move(@current, @board.selection, p)
217 @board.selection = nil
218 elsif movable?(state, p)
224 def on_board_drop(data)
227 @board.add_to_group data[:item]
228 @board.lower data[:item]
231 # board to board drop
232 if data[:src] == data[:dst]
233 # null drop, handle as a click
234 @board.selection = data[:src]
236 # normal move/premove
237 case policy.movable?(match.history.state, data[:src])
239 move = execute_move(data[:src], data[:dst], :adjust => true)
241 @premover.move(@current, data[:src], data[:dst])
244 elsif data[:index] and data[:dst]
246 case droppable?(match.history.state,
250 move = execute_drop(data[:item], data[:dst])
252 @premover.drop(@current, data[:pool_color], data[:index], data[:dst])
256 cancel_drop(data) unless move
259 def on_board_drag(data)
261 if movable?(match.history.state, data[:src])
262 @board.raise data[:item]
263 @board.remove_from_group data[:item]
264 data[:item].parent_item = nil
265 @board.selection = nil
270 def on_pool_drag(c, data)
272 if droppable?(match.history.state, c, data[:index])
273 # replace item with a correctly sized one
274 item = @board.create_piece(data[:item].name)
276 @board.remove_from_group item
277 item.parent_item = nil
278 anim = @pools[c].animator.remove_piece(data[:index])
280 data[:size] = @board.unit
281 data[:pool_color] = c
289 def on_pool_drop(color, data)
294 time.each do |pl, ms|
296 @clocks[pl].clock.set_time(ms)
298 @clocks[pl].clock ||= Clock.new(ms, 0, Qt::Timer)
304 @clocks.each do |pl, clock|
310 def add_controlled_player(player)
311 @controlled[player.color] = player
315 return true if x == self
316 @controlled.each do |c, p|
317 return true if x == p
323 @match.close if @match
328 @controlled = { @color => self }
335 if match && match.editable?
336 manager.undo(1, :allow_more => true)
348 def navigate(direction)
350 match.navigate(self, direction)
351 rescue History::OutOfBound
352 puts "error: out of bound"
355 def movable?(state, p)
356 result = policy.movable?(state, p)
357 return false unless result
358 return false unless result == :movable || @premove
359 return false unless @controlled[state.board[p].color]
360 return false if match.history.current < match.index and (not match.editable?)
364 def droppable?(state, color, index)
365 result = policy.droppable?(state, color, index)
366 return false unless result
367 return false unless result == :droppable || @premove
368 return false unless @controlled[color]
369 return false if match.history.current < match.index and (not match.editable?)
373 def perform!(move, opts = {})
374 turn = match.history.state.turn
375 match.move(self, move, opts)
376 fire :dirty unless @dirty
380 def cancel_drop(data)
381 anim = if data[:index]
382 # remove dragged item
384 # make original item reappear in its place
385 @pools[data[:pool_color]].animator.insert_piece(
389 @board.add_to_group data[:item]
390 @board.lower data[:item]
391 @animator.movement(data[:item], nil, data[:src], Path::Linear)
394 @field.run(anim) if anim
398 @pools.each do |col, pool|
399 anim = pool.animator.warp(match.history.state.pool(col))