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'
21 attr_reader :match, :policy
23 attr_reader :controlled
27 attr_accessor :premove
36 @field = AnimationField.new(20)
40 yield @board if @board
41 @pools.each {|c, pool| yield pool }
42 @clocks.each {|c, clock| yield clock }
47 @policy = match.game.policy.new
48 @current = match.history.current
51 @board = @table.elements[:board]
52 @pools = @table.elements[:pools]
53 @clocks = @table.elements[:clocks]
54 @premover = Premover.new(self, @board, @pools)
56 @animator = match.game.animator.new(@board)
57 @board.reset(match.state.board)
60 @clocks.each do |col, clock|
64 @board.observe(:click) {|p| on_board_click(p) }
65 @board.observe(:drag) {|data| on_board_drag(data) }
66 @board.observe(:drop) {|data| on_board_drop(data) }
67 @pools.each do |col, pool|
68 pool.observe(:drag) {|data| on_pool_drag(col, data) }
69 pool.observe(:drop) {|data| on_pool_drop(col, data) }
71 @clocks.each do |col, clock|
72 clock.data = { :color => col,
73 :player => match.player(col).name }
76 match.history.observe(:current_changed) { refresh }
77 match.history.observe(:truncate) { refresh :instant => true }
78 match.history.observe(:new_move) do |data|
80 @clocks.each do |player, clock|
81 if data[:state].turn == player
89 @clocks[match.game.players.first].active = true
90 @table.flip(@color && (@color != match.game.players.first))
93 @board.highlight(match.history.move)
100 rescue History::OutOfBound
101 puts "error: first move"
106 match.history.forward
107 rescue History::OutOfBound
108 puts "error: last move"
111 # sync displayed state with current history item
113 def refresh(opts = { })
115 index = match.history.current
116 return if index == @current
118 anim = @animator.warp(match.history.state, opts)
119 perform_animation anim
120 elsif index > @current
121 (@current + 1..index).each do |i|
122 animate(:forward, match.history[i].state, match.history[i].move, opts)
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)
130 @board.highlight(match.history[@current].move)
135 match.history.go_to(index)
136 rescue History::OutOfBound
137 puts "error: no such index #{index}"
140 def animate(direction, state, move, opts = {})
141 anim = @animator.send(direction, state, move, opts)
142 perform_animation anim
145 def perform_animation(anim)
150 def on_board_click(p)
152 state = match.history.state
153 # if there is a selection already, move or premove
154 # to the clicked square
156 case policy.movable?(match.history.state, @board.selection)
159 execute_move(@board.selection, p)
161 # schedule a premove on the board
162 @premover.move(@board.selection, p)
164 @board.selection = nil
165 elsif movable?(state, p)
171 def on_board_drop(data)
174 @board.add_to_group data[:item]
175 @board.lower data[:item]
178 # board to board drop
179 if data[:src] == data[:dst]
180 # null drop, handle as a click
181 @board.selection = data[:src]
183 # normal move/premove
184 case policy.movable?(match.history.state, data[:src])
186 move = execute_move(data[:src], data[:dst], :adjust => true)
188 @premover.move(data[:src], data[:dst])
191 elsif data[:index] and data[:dst]
193 case droppable?(match.history.state,
197 move = execute_drop(data[:item], data[:dst])
199 @premover.drop(data[:pool_color], data[:index], data[:dst])
203 cancel_drop(data) unless move
206 def on_board_drag(data)
208 if movable?(match.history.state, data[:src])
209 @board.raise data[:item]
210 @board.remove_from_group data[:item]
211 @board.selection = nil
216 def on_pool_drag(c, data)
218 if droppable?(match.history.state, c, data[:index])
219 # replace item with a correctly sized one
220 item = @board.create_piece(data[:item].name)
222 @board.remove_from_group item
223 anim = @pools[c].animator.remove_piece(data[:index])
225 data[:size] = @board.unit
226 data[:pool_color] = c
234 def on_pool_drop(color, data)
239 time.each do |pl, seconds|
240 @clocks[pl].clock ||= Clock.new(seconds, 0, nil)
241 @clocks[pl].clock.set_time(seconds)
246 @clocks.each do |pl, clock|
252 def add_controlled_player(player)
253 @controlled[player.color] = player
257 @match.close if @match
262 @controlled = { @color => self }
268 def movable?(state, p)
269 result = policy.movable?(state, p)
270 return false unless result
271 return false unless result == :movable || @premove
272 return false unless @controlled[state.board[p].color]
273 return false if match.history.current < match.index and (not match.editable?)
277 def droppable?(state, color, index)
278 result = policy.droppable?(state, color, index)
279 return false unless result
280 return false unless result == :droppable || @premove
281 return false unless @controlled[color]
282 return false if match.history.current < match.index and (not match.editable?)
286 def perform!(move, opts = {})
287 turn = match.history.state.turn
288 match.move(@controlled[turn], move, opts)
291 def cancel_drop(data)
292 anim = if data[:index]
293 # remove dragged item
295 # make original item reappear in its place
296 @pools[data[:pool_color]].animator.insert_piece(
300 @animator.movement(data[:item], nil, data[:src], Path::Linear)
303 @field.run(anim) if anim
307 @pools.each do |col, pool|
308 anim = pool.animator.warp(match.history.state.pool(col))