# == Synopsis
#
# Library to support Cartagena play
# 
# == Author
# Neil Smith
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# == Change history
# Version 1.0::  11 Jun 2008  
#                * Initial build

# Symbols
$SYMBOLS = [:bottle, :dagger, :gun, :hat, :keys, :skull]

# The number of cards of each symbol in the deck
$CARDS_PER_SYMBOL = 17

# The number of cards initiall dealt to each player
$INITIAL_CARDS_PER_PLAYER = 3

# Maximum number of pieces on a position
$MAX_PIECES_PER_POSITION = 3

# Errors for a game

# Moves can only be [1..6] spaces
class InvalidMoveError < StandardError
end

# Game is won when only one player has uncaptured pieces
class GameWonNotice < StandardError
end

# A position on the board.
class Position
  attr_reader :symbol, :contains
  
  def initialize(symbol)
    @symbol = symbol
    @contains = []
  end
end

# A tile that makes up the board.  Each tile has two sides, each with six
# positions.  Tiles can be either way up, and either way round.
class Tile
  attr_reader :exposed, :front, :back
  
  def initialize(front, back)
    @front = front.collect {|s| Position.new(s)}
    @back = back.collect {|s| Position.new(s)}
    @exposed = @front
    @exposed_side = :front
  end
  
  def flip!
    if @exposed_side == :front
      @exposed = @back
      @exposed_side = :back
    else
      @exposed = @front
      @exposed_side = :front
    end
  end
  
  def reverse!
    @exposed = @exposed.reverse
  end
end


# The game board
class Board

  attr_reader :positions
  attr_reader :tiles

  # A laborious procedure to create all the positions and tie them all together
  def initialize(tiles_used)
    # A hash of all positions, indexed by position names
    @tiles = [Tile.new([:hat, :keys, :gun, :bottle, :skull, :dagger], 
        [:hat, :keys, :gun, :bottle, :skull, :dagger]),
      Tile.new([:gun, :hat, :dagger, :skull, :bottle, :keys],
        [:gun, :hat, :dagger, :skull, :bottle, :keys]),
      Tile.new([:skull, :gun, :bottle, :keys, :dagger, :hat],
        [:skull, :gun, :bottle, :keys, :dagger, :hat]),
      Tile.new([:dagger, :bottle, :keys, :gun, :hat, :skull],
        [:dagger, :bottle, :keys, :gun, :hat, :skull]),
      Tile.new([:keys, :dagger, :skull, :hat, :gun, :bottle],
        [:keys, :dagger, :skull, :hat, :gun, :bottle]),
      Tile.new([:bottle, :skull, :hat, :dagger, :keys, :gun],
        [:bottle, :skull, :hat, :dagger, :keys, :gun])                 
    ].shuffle[0...tiles_used]
    @tiles = @tiles.each do |t|
      if rand < 0.5
        t.reverse!
      else
        t
      end
    end

    @positions = [Position.new(:cell)]
    @tiles.each {|t| t.exposed.each {|p| @positions << p}}
    @positions << Position.new(:boat)
  end # def

  def to_s
    layout
  end
  
  def to_str
    to_s
  end
  
  
  # For each position, show its name and what it touches
  def layout
    out_string = ""
    @positions.each {|position| out_string << "#{position.symbol}\n"}
    out_string
  end

end


# Each piece on the board is an object
class Piece
  attr_reader :player, :number
  attr_accessor :position

  def initialize(position, player, number)
    @position = position
    @player = player
    @number = number
  end

  def to_s
    "#{@player}:#{@number}"
  end
  
  def to_str
    to_s
  end

  def move_to(new_position)
    @position = new_position
  end

end


# A move in a game
class Move
  attr_reader :piece, :origin, :destination
  
  def initialize(piece, origin, destination)
    @piece = piece
    @origin = origin
    @destination = destination
  end

  def show(board)
    "#{@piece.to_s}: #{board.positions.index(@origin)} -> #{board.positions.index(@destination)}"
  end
  
  # Write a move to a string
  # Note the inverse, String#to_move, is defined below
  def to_s
    "#{@piece.player}: #{@origin.to_s} ->  #{@destination.to_s}"
  end
  
  def to_str
    to_s
  end
  
end



# A class to record each of the states previously found in a game.  
# Note that this is a deep copy of the pieces and what they've captured, so 
# you'll need to convert back
class GameState
  attr_accessor :move, :player, :pieces_after_move
  
  def initialize(move, player, pieces)
    @move = move
    @player = player
    @pieces_after_move = Hash.new
    pieces.each {|k, p| @pieces_after_move[k] = p.dup}
  end
  
  def ==(other)
    @move.to_s == other.move.to_s and
      @player == other.player and
      @piece_after_move == other.pieces_after_move
  end
  
end


# A game of Cartagena.  It keeps a history of all previous states.  
class Game

  attr_reader :history
  attr_reader :current_player, :players, :players_cards
  attr_reader :board
  attr_reader :pieces
  attr_reader :deck

  # Create a new game
  def initialize(players = 6, number_of_tiles = 6, pieces_each = 6)
    @board = Board.new(number_of_tiles)
    @history = []
    @pieces = []
    @players = [].fill(0, players) {|i| i}
    @players_cards = []
    @current_player = 0
    @cards = []
    1.upto($CARDS_PER_SYMBOL) {|x| @cards.concat($SYMBOLS)}
    @deck = @cards.shuffle
    @players.each do |p|
      @pieces[p] = []
      0.upto(pieces_each - 1) do |count|
        piece = Piece.new(@board.positions[0], p, count)
        @pieces[p][count] = piece
        @board.positions[0].contains << piece
      end
      @players_cards[p] = []
      deal_cards!($INITIAL_CARDS_PER_PLAYER, p)
    end
  end
  
  def deal_cards!(number_of_cards, player = @current_player)
    1.upto(number_of_cards) do
      @deck = @cards.shuffle if @deck.empty?
      @players_cards[player] << @deck.pop
    end
  end
  
  # Check that a move is valid.  Throw an exception if it's invalid
  def validate_move(move, player = @current_player)
    # Check the move is a valid one
    raise(InvalidMoveError, "Move #{move}: Player #{player} does not exist") unless @players.include?(player)
    raise(InvalidMoveError, "Move #{move}: None of player #{player}'s pieces on position #{move.origin}") unless move.origin.contains.find {|pc| pc.player == player}
    raise(InvalidMoveError, "Move #{move}: Origin and destination are the same") if move.origin == move.destination
  
    origin_position      = @board.positions.index(move.origin)
    destination_position = @board.positions.index(move.destination)
    # Is this move an advance or a retreat?
    if destination_position > origin_position
      # Advancing a piece
      unless @players_cards[player].find {|c| c == move.destination.symbol}
        raise(InvalidMoveError, "Player #{player} does not have a card to move a piece to a #{move.destination.symbol} square")
      end
      # Check target square is vacant
      raise(InvalidMoveError, "Advance move #{move}: destination occupied") unless move.destination.contains.empty?
      # Check all the intervening squares with this symbol are occupied
      intervening_empty_position = @board.positions[origin_position...destination_position].index_find do |p| 
          p.symbol == move.destination.symbol and 
          p.contains.empty?
      end
      raise(InvalidMoveError, "Advance move #{move}: location #{intervening_empty_position} is empty") if intervening_empty_position
    else
      # Retreating a piece
      # Check target position has one or two pieces already on it
      destination_count = move.destination.contains.length
      raise(InvalidMoveError, "Retreat move #{move}: destination has no pieces already on it") if destination_count == 0
      raise(InvalidMoveError, "Retreat move #{move}: destination has too many (#{destination_count}) pieces already on it") if destination_count >= $MAX_PIECES_PER_POSITION
      # Check none of the intervening squares have any pieces on them
# puts "Checking positions #{destination_position} to #{origin_position}"      
      intervening_target_position = @board.positions[(destination_position + 1)...origin_position].index_find do |p| 
# puts "Examining postition #{p} at location #{@board.positions.index(p)} which contains #{p.contains.length} pieces"        
          p.contains.length > 0 and 
          p.contains.length < $MAX_PIECES_PER_POSITION
      end
      raise(InvalidMoveError, "Retreat move #{move.show(@board)}: location #{intervening_target_position} is a viable target") if intervening_target_position
    end
  end
  
  # Apply a single move to a game.  
  def apply_move!(move, player = @current_player)
    
    validate_move(move, player)
    
    # Apply this move
    move.origin.contains.delete move.piece
    move.destination.contains << move.piece
    move.piece.position = move.destination

    # Update cards
    if @board.positions.index(move.destination) > @board.positions.index(move.origin)
      # Advance move
      @players_cards[player].delete_at(@players_cards[player].index(move.destination.symbol))
    else
      # Retreat move
      deal_cards!(move.destination.contains.length - 1, player)
    end

    # Record the new stae
#    this_game_state = GameState.new(move, player, @pieces)
#    @history << this_game_state

    # If this player has all their pieces in the boat, declare a win.
    if @pieces[player].all? {|pc| pc.position == :boat}
      raise(GameWonNotice, "Game won by #{player}")
    end
  end
  
  # Undo a move 
  def undo_move!
    if @history.length > 1
      # general case
      state_to_restore = @history[-2]
      @current_player = @history[-1].player
      @pieces.each do |name, piece| 
        copy_piece = state_to_restore.pieces_after_move[name]
        piece.position = copy_piece.position
      end
      @history.pop    
    elsif @history.length == 1
      # reset to start
      @current_player = @players[1]
      @pieces.each do |name, piece| 
        piece.position = @board.positions[piece.colour]
      end
      @history.pop    
    end
  end
    
  # Apply a list of moves in order
  def apply_moves!(moves)
    moves.each do |move| 
      if move.via_base?
        moved_distance = board.distance_between[move.piece.position.place][@current_player] +
          board.distance_between[@current_player][move.destination.place]
      else
        moved_distance = board.distance_between[move.piece.position.place][move.destination.place]
      end
      self.apply_move!(move, @current_player)
      next_player! unless moved_distance == 6
    end
  end


  # Set the current player to be the next player
  def next_player!
    original_player = @current_player
    if @current_player == @players[-1]
      @current_player = @players[1]
    else
      @current_player = @players[@players.index(@current_player) + 1]
    end
    @current_player
  end


  # Return an array of all possible moves from this state, given the active player
  def possible_moves(player = @current_player)
    moves = []
    @pieces[player].each do |piece|
      # Do a forward move for each card held
      unless piece.position == @board.positions[-1]
        @players_cards[player].each do |card|
          destination = @board.positions[@board.positions.index(piece.position)..-1].find do |pos|
            (pos.symbol == card and pos.contains == []) or
              pos.symbol == :boat
          end
<<<<<<< .mine
puts "Player #{player}, card #{card}, piece #{piece.number} at position #{@board.positions.index(piece.position)} moving to #{@board.positions.index(destination)}, a #{destination.symbol}"        
=======
# puts "Player #{player}, card #{card}, piece #{piece.number} at position #{@board.positions.index(piece.position)} to #{@board.positions.index(destination)}, a #{destination.symbol}"        
>>>>>>> .r34
          moves << Move.new(piece, piece.position, destination)
puts "Added move #{piece.number}: #{piece.position.symbol} -> #{destination.symbol}"
        end
      end
      # Do a reverse move for the piece
      unless piece.position == board.positions[0]
        destination = @board.positions[0...@board.positions.index(piece.position)].reverse.find do |pos|
          pos.contains.length == 1 or pos.contains.length == 2
        end
        if destination
# puts "Player #{player}, piece #{piece.number} at position #{@board.positions.index(piece.position)} retreats to #{@board.positions.index(destination)}, a #{destination.symbol} containing #{destination.contains.length} pieces"
          moves << Move.new(piece, piece.position, destination)
        end
      end
    end
# moves.each {|m| puts m.show(@board)}    
    moves
  end

  def build_state_string
    outstr = "Current player = #{@current_player}\n"
    0.upto((@board.positions.length)-1) do |i|
      outstr << "#{i}: #{@board.positions[i].symbol}: "
      @board.positions[i].contains.each do |piece|
        outstr << "P#{piece.player}:#{piece.number} "
      end
      outstr << "\n"
    end
    0.upto((@players.length)-1) do |i|
      outstr << "Player #{i} holds " << (@players_cards[i].sort_by {|c| c.to_s}).join(', ') << "\n"
    end
    outstr << "Deck holds " << @deck.join(', ') << "\n"
    outstr
  end
  
  # Show the state of the board
  def show_state
    puts build_state_string
    #    @pieces.keys.sort.each do |piece_name|
    #      if @pieces[piece_name].captured
    #        puts "Piece #{piece_name} captured, at #{@pieces[piece_name].position}"
    #      else
    #        puts "Piece #{piece_name} is at #{@pieces[piece_name].position}, holds #{(@pieces[piece_name].contains.collect{|c| c.name}).join(' ')}"
    #      end
    #    end
  end

  def to_s
    show_state
  end
  
  def to_str
    to_s
  end
  

  def set_testing_game!
    srand 1234
    @board = nil
    initialize(6, 6, 6)
  end
  
  # Given a set of lines from an input file, turn them into a Game object and 
  # a set of Move objects.
  # Note the multiple return values.
  # Class method
  def Game.read_game(gamelines)
    gamelines.each {|l| l.chomp!}
    game = Game.new(gamelines[0].to_i, 6, 6, 6, 6)
    moves = []
    gamelines[1..-2].each {|m| moves << m.to_move(game)}
    return game, moves, gamelines[-1].to_i
  end


end
  

# Extension to String class to convert a move-description string into a Move object.  
# This is the inverse of the Move#to_s method
class String
  def to_move(game)
    move_elements = self.downcase.split
    piece_name = move_elements[0]
    destination_name = move_elements[-1]
    if destination_name.length > 2 and 
        destination_name[-2,2] == game.board.centre.place[-2,2]
      destination_name = game.board.centre.place
    end
    raise(InvalidMoveError, "Invalid piece in move read") unless game.pieces.has_key?(piece_name)
    raise(InvalidMoveError, "Invalid destination in move read") unless game.board.positions.has_key?(destination_name)
    # Deal with the synonyms for the centre position
    via_base = (destination_name.length == 1 or move_elements.length > 2)
    Move.new(game.pieces[piece_name], game.board.positions[destination_name], via_base)
  end
end


# Read a game description file and convert it into a Game object and set of Move objects.
# Note that Game.read_game method returns multiple values, so this one does too.
class IO
  def IO.read_game(filename)
    gamelines = IO.readlines(filename)
    return Game.read_game(gamelines)
  end
end

# Extension to the Array class to include the Array#shuffle function
class Array
  def shuffle
    sort_by { rand }
  end
  
  def index_find(&block)
    found = find(&block)
    if found
      index found
    else
      found
    end
  end
end