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 'interaction/undo_manager'
17 "<#{name}:#{self.class.name}>"
28 GameNotStarted = Class.new(Exception)
34 def initialize(game, opts = {})
36 @players = { } # player => ready
38 @kind = opts[:kind] || :local
45 return false if @history
46 return false if @players.has_key?(player)
47 return false if complete?
48 return false unless @game.players.include?(player.color)
50 @players[player] = false
51 fire :complete if complete?
56 return false if @history
57 return false unless complete?
58 return false unless @players[player] == false
60 @players[player] = true
61 if @players.values.all?
62 state = @game.state.new
64 @history = History.new(state)
71 def navigate(player, direction)
72 navigate_internal(player, direction, [])
75 def go_to(player, index)
76 navigate_internal(player, :go_to, [index],
78 :old_index => history.current)
81 def move(player, move, opts = {})
84 warn "a closed match is still active"
86 return false unless @history
88 # if the match is non-editable, jump to the last move
93 # if player is nil, assume the current player is moving
95 player = current_player
97 return false unless is_playing?(player)
100 validate = @game.validator.new(@history.state)
101 valid = validate[move]
103 warn "Invalid move from #{player.name}: #{move}"
107 old_state = @history.state
108 state = old_state.dup
110 @history.add_move(state, move, opts)
112 broadcast player, :move => {
116 :old_state => old_state }
122 @manager = UndoManager.new(@players.keys)
123 @manager.on(:execute) do |moves|
131 @manager.undo(player, 1, :allow_more => true)
133 # request permission from other players
134 @players.keys.each do |p|
135 p.allow_undo?(p, @manager) unless p == player
145 def update_time(time)
146 broadcast nil, :time => time
150 @game.players.all? do |c|
151 @players.keys.find {|p| p.color == c }
164 @history[index].state
168 @opts.fetch(:editable, true)
172 @opts.fetch(:navigable, false)
176 @opts.fetch(:time_running, false) and
181 @history.valid_index?(index)
185 @players.keys.find{|p| p.color == color }
193 # players must not send any more 'move' events to
196 def close(result = nil, message = nil)
198 @info[:result] = result if result
199 broadcast nil, :close => {
201 :message => message }
206 @info.merge(:players => @players.keys)
210 @info = @info.merge(infos)
211 infos[:players].each do |col, name|
228 @history or raise(GameNotStarted)
231 def history=(history)
237 def broadcast(player, event)
240 @players.each_key do |p|
241 p.update any_to_event(event) unless p == player
246 @manager.cancel if @manager
250 def navigate_internal(player, method, history_args, event_args = {})
252 awaiting_server = false
254 history.send(method, *history_args)
255 rescue History::OutOfBound => e
256 # if navigable, the history may be missing
257 # items, they will be added later
258 raise e unless navigable?
259 awaiting_server = true
263 method => event_args.merge(:awaiting_server => awaiting_server)
267 def is_playing?(player)
268 @players.each_key do |p|
269 return true if player.controls?(p)