# coding: utf-8 # frozen_string_literal: true require_relative './util/constants' require_relative 'invalid_move_exception' # Methoden, welche die Spielregeln von Hive abbilden. # # Es gibt hier viele Helfermethoden, die von den beiden Hauptmethoden {GameRuleLogic#valid_move?} und {GameRuleLogic.possible_moves} benutzt werden. class GameRuleLogic include Constants # Fügt einem leeren Spielfeld drei Brombeeren hinzu. # # Diese Methode ist dazu gedacht, ein initiales Spielbrett regelkonform zu generieren. # # @param board [Board] Das zu modifizierende Spielbrett. Es wird nicht # geprüft, ob sich auf dem Spielbrett bereits Brombeeren befinden. # @return [Board] Das modifizierte Spielbrett. def self.add_blocked_fields(board) raise "todo" board end def self.get_neighbour_in_direction(board, coords, direction) board.field_at(direction.translate(coords)) end def self.get_neighbours(board, coordinates) Direction.map { |d| get_neighbour_in_direction(board, coordinates, d) }.compact end def self.is_bee_blocked(board, color) bee_fields = board.field_list.select { |f| f.pieces.include?(Piece.new(color, PieceType::BEE)) } return false if bee_fields.empty? return get_neighbours(board, bee_fields[0].coordinates).all? { |f| !f.empty? } end # Prueft, ob ein Spielzug fuer den gegebenen Gamestate valide ist # # @param gamestate [Gamestate] # @param move [Move] # @return [?] def self.valid_move?(gamestate, move) case move when SetMove validate_set_move(gamestate, move) when DragMove validate_drag_move(gamestate, move) when SkipMove validate_skip_move(gamestate, move) end end def self.is_on_board(coords) shift = (BOARD_SIZE - 1) / 2 -shift <= coords.x && coords.x <= shift && -shift <= coords.y && coords.y <= shift end def self.has_player_placed_bee(gamestate) gamestate.deployed_pieces(gamestate.current_player_color).any? { |p| p.type == PieceType::BEE } end def self.validate_set_move(gamestate, move) unless is_on_board(move.destination) raise InvalidMoveException.new("Piece has to be placed on board. Destination ${move.destination} is out of bounds.", move) end unless gamestate.board.field_at(move.destination).empty? raise InvalidMoveException.new("Set destination is not empty!", move) end owned_fields = gamestate.board.fields_of_color(gamestate.current_player_color) if owned_fields.empty? other_player_fields = gamestate.board.fields_of_color(gamestate.other_player_color) if !other_player_fields.empty? unless other_player_fields.map{ |of| get_neighbours(gamestate.board, of.coordinates).map{ |n| n.coordinates } }.flatten.include?(move.destination) raise InvalidMoveException.new("Piece has to be placed next to other players piece", move) end end else if gamestate.round == 3 && !has_player_placed_bee(gamestate) && move.piece.type != PieceType::BEE raise InvalidMoveException.new("The bee must be placed in fourth round latest", move) end if !gamestate.undeployed_pieces(gamestate.current_player_color).include?(move.piece) raise InvalidMoveException.new("Piece is not a undeployed piece of the current player", move) end destination_neighbours = get_neighbours(gamestate.board, move.destination) if !destination_neighbours.any? { |f| f.color == gamestate.current_player_color } raise InvalidMoveException.new("A newly placed piece must touch an own piece", move) end if destination_neighbours.any? { |f| f.color == gamestate.other_player_color } raise InvalidMoveException.new("A newly placed is not allowed to touch an opponent's piece", move) end end true end def self.validate_skip_move(gamestate, move) if !possible_moves(gamestate).empty? raise InvalidMoveException.new("Skipping a turn is only allowed when no other moves can be made.", move) end if gamestate.round == 3 && !has_player_placed_bee(gamestate) raise InvalidMoveException.new("The bee must be placed in fourth round latest", move) end true end def self.validate_drag_move(gamestate, move) unless has_player_placed_bee(gamestate) raise InvalidMoveException.new("You have to place the bee to be able to perform dragmoves", move) end if (!is_on_board(move.destination) || !is_on_board(move.start)) raise InvalidMoveException.new("The Move is out of bounds", move) end if (gamestate.board.field_at(move.start).pieces.empty?) raise InvalidMoveException.new("There is no piece to move", move) end piece_to_drag = gamestate.board.field_at(move.start).pieces.last if (piece_to_drag.owner != gamestate.current_player_color) raise InvalidMoveException.new("Trying to move piece of the other player", move) end if (move.start == move.destination) raise InvalidMoveException.new("Destination and start are equal", move) end if (!gamestate.board.field_at(move.destination).pieces.empty? && piece_to_drag.type != PieceType::BEETLE) raise InvalidMoveException.new("Only beetles are allowed to climb on other Pieces", move) end board_without_piece = gamestate.board.clone board_without_piece.field_at(move.start).pieces.pop if (!is_swarm_connected(board_without_piece)) raise InvalidMoveException.new("Moving piece would disconnect swarm", move) end case piece_to_drag.type when PieceType::ANT validate_ant_move(board_without_piece, move) when PieceType::BEE validate_bee_move(board_without_piece, move) when PieceType::BEETLE validate_beetle_move(board_without_piece, move) when PieceType::GRASSHOPPER validate_grasshopper_move(board_without_piece, move) when PieceType::SPIDER validate_spider_move(board_without_piece, move) end true end def self.validate_ant_move(board, move) visited_fields = [move.start] index = 0 while index < visited_fields.size current_field = visited_fields[index] new_fields = accessible_neighbours_except(board, current_field, move.start).reject { |f| visited_fields.include? f } return true if new_fields.map(&:coordinates).include?(move.destination) visited_fields += new_fields index += 1 end raise InvalidMoveException.new("No path found for ant move", move) end def self.is_swarm_connected(board) board_fields = board.field_list.select{ |f| !f.pieces.empty? } return true if board_fields.empty? visited_fields = board_fields.take 1 total_pieces = board.pieces.size index = 0 while index < visited_fields.size current_field = visited_fields[index] occupied_neighbours = get_neighbours(board, current_field.coordinates) .filter { |f| !f.pieces.empty? } occupied_neighbours -= visited_fields visited_fields += occupied_neighbours return true if visited_fields.sum{ |f| f.pieces.size } == total_pieces index += 1 end false end def self.validate_beetle_move(board, move) validate_destination_next_to_start(move) if ((shared_neighbours_of_two_coords(board, move.start, move.destination) + [board.field_at(move.destination), board.field_at(move.start)]).all? { |f| f.pieces.empty? }) raise InvalidMoveException.new("Beetle has to move along swarm", move) end end def self.validate_destination_next_to_start(move) if (!is_neighbour(move.start, move.destination)) raise InvalidMoveException.new("Destination field is not next to start field", move) end end def self.is_neighbour(start, destination) Direction.map do |d| d.translate(start) end.include?(destination) end def self.shared_neighbours_of_two_coords(board, first_coords, second_coords) get_neighbours(board, first_coords) & get_neighbours(board, second_coords) end def self.validate_bee_move(board, move) validate_destination_next_to_start(move) if (!can_move_between(board, move.start, move.destination)) raise InvalidMoveException.new("There is no path to your destination", move) end end def self.can_move_between(board, coords1, coords2) shared = shared_neighbours_of_two_coords(board, coords1, coords2) (shared.size == 1 || shared.any? { |n| n.empty? && !n.obstructed }) && shared.any? { |n| !n.pieces.empty? } end def self.validate_grasshopper_move(board, move) if (!two_fields_on_one_straight(move.start, move.destination)) raise InvalidMoveException.new("Grasshopper can only move straight lines", move) end if (is_neighbour(move.start, move.destination)) raise InvalidMoveException.new("Grasshopper has to jump over at least one piece", move) end if (get_line_between_coords(board, move.start, move.destination).any? { |f| f.empty? }) raise InvalidMoveException.new("Grasshopper can only jump over occupied fields, not empty ones", move) end end def self.two_fields_on_one_straight(coords1, coords2) return coords1.x == coords2.x || coords1.y == coords2.y || coords1.z == coords2.z end def self.get_line_between_coords(board, start, destination) if (!two_fields_on_one_straight(start, destination)) raise InvalidMoveException.new("destination is not in line with start") end # TODO use Direction shift dX = start.x - destination.x dY = start.y - destination.y dZ = start.z - destination.z d = (dX == 0) ? dY.abs : dX.abs (1..(d-1)).to_a.map do |i| board.field_at( CubeCoordinates.new( destination.x + i * (dX <=> 0), destination.y + i * (dY <=> 0), destination.z + i * (dZ <=> 0) ) ) end end def self.accessible_neighbours_except(board, start, except) get_neighbours(board, start).filter do |neighbour| neighbour.empty? && can_move_between_except(board, start, neighbour, except) && neighbour.coordinates != except end end def self.can_move_between_except(board, coords1, coords2, except) shared = shared_neighbours_of_two_coords(board, coords1, coords2).reject do |f| f.pieces.size == 1 && except == f.coordinates end (shared.size == 1 || shared.any? { |s| s.empty? && !s.obstructed }) && shared.any? { |s| !s.pieces.empty? } end def self.validate_spider_move(board, move) found = get_accessible_neighbours(board, move.start).any? do |depth_one| get_accessible_neighbours_except(board, depth_one, move.start).any? do |depth_two| get_accessible_neighbours_except(board, depth_two, move.start).reject{ |f| f.coordinates == depth_one.coordinates }.any? { |f| move.destination == f.coordinates } end end return true if (found) raise InvalidMoveException.new("No path found for spider move", move) end def self.get_accessible_neighbours(board, start) get_neighbours(board, start).filter do |neighbour| neighbour.empty? && can_move_between(board, start, neighbour) end end def self.get_accessible_neighbours_except(board, start, except) get_neighbours(board, start).filter do |neighbour| neighbour.empty? && can_move_between_except(board, start, neighbour, except) && neighbour.coordinates != except end end def self.perform_move(gamestate, move) raise "Invalid move!" unless valid_move?(gamestate, move) case move when SetMove # delete first occurrence of piece gamestate.undeployed_pieces(move.piece.color).delete_at( gamestate.undeployed_pieces(move.piece.color).index(move.piece) || gamestate.undeployed_pieces(move.piece.color).length ) gamestate.board.field_at(move.destination).add_piece(move.piece) when DragMove piece_to_move = gamestate.board.field_at(move.start).remove_piece gamestate.board.field_at(move.destination).add_piece(piece_to_move) end gamestate.turn += 1 gamestate.last_move = move end # all possible moves, but will *not* return the skip move if no other moves are possible! def self.possible_moves(gamestate) possible_set_moves(gamestate) + possible_drag_moves(gamestate) end def self.possible_drag_moves(gamestate) gamestate.board.fields_of_color(gamestate.current_player_color).flat_map do |start_field| edge_targets = empty_fields_connected_to_swarm(gamestate.board) additional_targets = if start_field.pieces.last.type == PieceType::BEETLE get_neighbours(gamestate.board, start_field).uniq else [] end edge_targets + additional_targets.map do |destination| move = DragMove.new(start_field, destination) begin valid_move?(gamestate, move) move rescue InvalidMoveException nil end end.compact end end def self.empty_fields_connected_to_swarm(board) board.field_list .filter { |f| f.has_owner } .flat_map { |f| get_neighbours(board, f).filter { f.empty? } } .uniq end def self.possible_set_move_destinations(board, owner) board.fields_of_color(owner) .flat_map { |f| get_neighbours(board, f).filter { |f| f.empty? } } .uniq .filter { |f| get_neighbours(board, f).all? { |n| n.color != owner.opponent } } end def self.possible_set_moves(gamestate) undeployed = gamestate.undeployed_pieces(gamestate.current_player_color) set_destinations = if (undeployed.size == STARTING_PIECES.size) # current player has not placed any pieces yet (first or second turn) if (gamestate.undeployed_pieces(gamestate.other_player_color).size == STARTING_PIECES.size) # other player also has not placed any pieces yet (first turn, all destinations allowed (except obstructed) gamestate.board.field_list.filter { |f| f.empty? } else # other player placed a piece already gamestate.board .fields_of_color(gamestate.other_player_color) .flat_map do |f| GameRuleLogic.get_neighbours(gamestate.board, f).filter(&:empty?) end end else possible_set_move_destinations(gamestate.board, gamestate.current_player_color) end possible_piece_types = if (!has_player_placed_bee(gamestate) && gamestate.turn > 5) [PieceType::BEE] else undeployed.map(&:type).uniq end set_destinations .flat_map do |d| possible_piece_types.map do |u| SetMove.new(Piece.new(gamestate.current_player_color, u), d) end end end # Prueft, ob ein Spieler im gegebenen GameState gewonnen hat. # @param gamestate [GameState] Der zu untersuchende GameState. # @return [Condition] nil, if the game is not won or a Condition indicating the winning player def self.winning_condition(gamestate) raise "Not implemented yet!" winner_by_single_swarm = [PlayerColor::RED, PlayerColor::BLUE].select do |player_color| GameRuleLogic.swarm_size(gamestate.board, player_color) == gamestate.board.fields_of_type(PlayerColor.field_type(player_color)).size end if winner_by_single_swarm.any? && gamestate.turn.even? return Condition.new(nil, "Unentschieden.") if winner_by_single_swarm.size == 2 return Condition.new(winner_by_single_swarm.first, "Schwarm wurde vereint.") end player_with_biggest_swarm = [PlayerColor::RED, PlayerColor::BLUE].sort_by do |player_color| GameRuleLogic.swarm_size(gamestate.board, player_color) end.reverse.first return Condition.new(player_with_biggest_swarm, "Rundenlimit erreicht, Schwarm mit den meisten Fischen gewinnt.") if gamestate.turn == 60 nil end end