# coding: utf-8 require_relative 'field_type' require_relative 'card_type' # All methods which define the game rules. Needed for checking validity of moves # and performing them. class GameRules # Berechnet wie viele Karotten für einen Zug der länge # moveCount benötigt werden. # # @param moveCount Anzahl der Felder, um die bewegt wird # @return Anzahl der benötigten Karotten def self.calculate_carrots(moveCount) (moveCount * (moveCount + 1)) / 2 end # Berechnet, wieviele Züge mit carrots Karotten möglich sind. # # @param carrots maximal ausgegebene Karotten # @return Felder um die maximal bewegt werden kann def self.calculate_movable_fields(carrots) # bei 30 Runden koennen nur 990 Karotten gesammelt werden return 44 if carrots >= 990 return 0 if carrots < 1 (Math.sqrt(2 * carrots + 1/4) - 1/2).round end # Überprüft Advance Aktionen auf ihre Korrektheit. Folgende # Spielregeln werden beachtet: # # - Der Spieler muss genügend Karotten für den Zug besitzen # - Wenn das Ziel erreicht wird, darf der Spieler nach dem Zug maximal 10 Karotten übrig haben # - Man darf nicht auf Igelfelder ziehen # - Salatfelder dürfen nur betreten werden, wenn man noch Salate essen muss # - Hasenfelder dürfen nur betreten werden, wenn man noch Hasenkarten ausspielen kann # # @param state GameState # @param distance relativer Abstand zur aktuellen Position des Spielers # @return [true, ''] falls ein Vorwärtszug möglich ist, [false, M] falls nicht, wobei M ein String mit einer Begruendung ist def self.is_valid_to_advance(state, distance) return false, 'Ein Vorwärtszug benötigt eine Mindestdistanz von einem Feld.' if distance <= 0 player = state.current_player return false, 'Es muss ein Salat gegessen werden, bevor ein Vorwärtszug gemacht werden kann.' if must_eat_salad(state) required_carrots = GameRules.calculate_carrots(distance) return false, "Nicht genug Karotten, um #{distance} Felder vorwärts zu ziehen (Vorrat: #{player.carrots}, benötigt: #{required_carrots}" if (required_carrots > player.carrots) new_position = player.index + distance return false, 'Zielfeld wird von anderem Spieler besetzt.' if state.occupied_by_other_player?(state.field(new_position)) case state.field(new_position).type when FieldType::INVALID return false, "Zielfeld #{new_position} ist nicht vorhanden." when FieldType::SALAD return false, 'Ohne Salat darf ein Salatfeld nicht betreten werden.' if player.salads < 1 when FieldType::HARE state2 = state.deep_clone state2.set_last_action(Advance.new(distance)) state2.current_player.index = new_position state2.current_player.carrots -= required_carrots return false, 'Auf ein Hasenfeld darf nur gezogen werden, wenn eine Karte gespielt werden kann' unless can_play_any_card(state2) when FieldType::GOAL carrotsLeft = player.carrots - required_carrots return false, "Auf das Zielfeld darf nur mit maximal 10 Karotten gezogen werden (es sind aber #{carrotsLeft} bei Erreichen des Zielfeldes)." unless carrotsLeft <= 10 return false, "Auf das Zielfeld darf nur ohne Salate gezogen werden (es sind aber #{player.salads} übrig)." unless player.salads == 0 when FieldType::HEDGEHOG return false, 'Auf ein Igelfeld darf nicht vorwärts gezogen werden.' when FieldType::CARROT, FieldType::POSITION_1, FieldType::START, FieldType::POSITION_2 return true, '' else raise "Unknown Type #{state.field(new_position).type.inspect}" end return true, '' end # Überprüft, ob ein Spieler aussetzen darf. Er darf dies, wenn kein anderer Zug möglich ist. # @param state GameState # @return true, falls der derzeitige Spieler keine andere Aktion machen kann. def self.is_valid_to_skip(state) return !GameRules.can_do_anything(state) end # Überprüft, ob ein Spieler einen Zug (keinen Aussetzug) # @param state GameState # @return true, falls ein Zug möglich ist. def self.can_do_anything(state) return can_play_any_card(state) || is_valid_to_fall_back(state)[0] || is_valid_to_exchange_carrots(state, 10)[0] || is_valid_to_exchange_carrots(state, -10)[0] || is_valid_to_eat(state)[0] || can_advance_to_any_field(state) end # Überprüft ob der derzeitige Spieler zu irgendeinem Feld einen Vorwärtszug machen kann. # @param state GameState # @return true, falls der Spieler irgendeinen Vorwärtszug machen kann def self.can_advance_to_any_field(state) fields = calculate_movable_fields(state.getCurrentPlayer().getCarrots()) (0..fields).to_a.any? do |i| is_valid_to_advance(state, i)[0] end end # Überprüft EatSalad Züge auf Korrektheit. Um einen Salat # zu verzehren muss der Spieler sich: # # - auf einem Salatfeld befinden # - noch mindestens einen Salat besitzen # - vorher kein Salat auf diesem Feld verzehrt wurde # # @param state GameState # @return [true, ''], falls ein Salad gegessen werden darf, [false, M] falls nicht, wobei M ein String mit dem Grund ist def self.is_valid_to_eat(state) return false, 'Salate dürfen nur auf Salatfeldern gegessen werden.' unless state.current_field.type == FieldType::SALAD return false, 'Spieler hat keine Salate mehr zum Essen.' if state.current_player.salads < 1 return false, 'Spieler muss das Feld verlassen.' if player_must_advance(state) return true, '' end # Überprüft ab der derzeitige Spieler im nächsten Zug einen Vorwärtszug machen muss. # @param state GameState # @return true, falls der derzeitige Spieler einen Vorwärtszug gemacht werden muss def self.player_must_advance(state) player = state.current_player type = state.board.field(player.index).type return true if (type == FieldType::HEDGEHOG || type == FieldType::START) last_action = state.current_player.last_non_skip_action if (!last_action.nil?) if (last_action.instance_of? EatSalad) return true elsif (last_action.instance_of? Card) # the player has to leave a rabbit field in next turn if (last_action.type == CardType::EAT_SALAD) return true elsif (last_action.type == CardType::TAKE_OR_DROP_CARROTS) # the player has to leave the rabbit field return true end end end return false end # Überprüft ob der derzeitige Spieler 10 Karotten nehmen oder abgeben kann. # @param state GameState # @param n 10 oder -10 je nach Fragestellung # @return [true, ''], falls die durch n spezifizierte Aktion möglich ist, [false, M] falls nicht, wobei M ein String mit dem Grund ist def self.is_valid_to_exchange_carrots(state, n) player = state.current_player return false, "Karotten können nur auf einem Karottenfeld #{n > 0 ? 'genommen' : 'abgegeben'} werden" if state.board.field(player.index).type != FieldType::CARROT return true, '' if n == 10 return false, 'Gültige Karottenzahlen sind 10 und -10.' unless n == -10 return false, "Spieler hat keine 10 Karotten zum Abgeben (er hat #{player.carrots})." if player.carrots < 10 return true, '' end # Überprüft FallBack Züge auf Korrektheit # # @param state GameState # @return [true, ''], falls der currentPlayer einen Rückzug machen darf, [false, M] falls nicht, wobei M ein String mit dem Grund ist. def self.is_valid_to_fall_back(state) return false, 'Spieler muss einen Salat fressen.' if must_eat_salad(state) target_field = state.previous_field_of_type( FieldType::HEDGEHOG, state.current_player.index ) return false, 'Es gibt kein Igelfeld hinter dem Spieler.' if target_field.nil? return false, 'Das Igelfeld hinter dem Spieler ist besetzt.' if state.occupied_by_other_player?(target_field) return true, '' end # Überprüft ob der derzeitige Spieler die FALL_BACK Karte spielen darf. # @param state GameState # @return [true, ''], falls die FALL_BACK Karte gespielt werden darf, [false, M] falls nicht, wobei M ein String mit dem Grund ist. def self.is_valid_to_play_fall_back(state) player = state.current_player return false, 'Spieler muss einen Vorwärtszug machen.' if player_must_advance(state) return false, 'Karten können nur auf Hasenfeldern gespielt werden.' unless state.current_field.type == FieldType::HARE return false, 'Nur der erste Spieler darf die FALL_BACK Karte spielen.' unless state.is_first(player) return false, 'Spieler besitzt die Karte FALL_BACK nicht.' unless player.owns_card_of_type(CardType::FALL_BACK) next_pos = state.other_player.index - 1 case state.field(next_pos).type when FieldType::INVALID return false, 'Durch Spielen der FALL_BACK Karte darf man nicht auf einem nicht vorhandenen Feld landen (also vor dem Start).' when FieldType::HEDGEHOG return false, 'Durch Spielen der FALL_BACK Karte darf man nicht auf einem Igelfeld landen.' when FieldType::SALAD return false, 'Spieler käme durch Spielen der FALL_BACK Kart auf ein Salatfeld, hat aber keine Salate.' if player.salads < 1 when FieldType::HARE state2 = state.deep_clone state2.set_last_action(Card.new(CardType::HURRY_AHEAD)) state2.current_player.cards.delete(CardType::FALL_BACK) return false, 'Spieler käme durch Spielen der FALL_BACK Kart auf ein Hasenfeld, kann aber dann keine weitere Karte mehr spielen.' unless can_play_any_card(state2) when FieldType::START when FieldType::CARROT when FieldType::POSITION_1 when FieldType::POSITION_2 return true, '' when FieldType::GOAL raise 'Player got onto goal by playing a fall back card. This should never happen.' else raise "Unknown Type #{state.field(next_pos).type.inspect}" end return true, '' end # Überprüft ob der derzeitige Spieler die HURRY_AHEAD Karte spielen darf. # @param state GameState # @return [true, ''], falls die HURRY_AHEAD Karte gespielt werden darf, [false, M], falls nicht, wobei M ein String mit dem Grund ist. def self.is_valid_to_play_hurry_ahead(state) player = state.current_player return false, 'Spieler muss einen Vorwärtszug machen.' if player_must_advance(state) return false, 'Karten können nur auf Hasenfeldern gespielt werden.' unless state.current_field.type == FieldType::HARE return false, 'Nur der zweite Spieler darf die HURRY_AHEAD Karte spielen.' unless state.is_second(player) return false, 'Spieler besitzt die Karte HURRY_AHEAD nicht.' unless player.owns_card_of_type(CardType::HURRY_AHEAD) o = state.other_player next_pos = o.index + 1 case state.field(next_pos).type when FieldType::INVALID return false, 'Durch Spielen der HURRY_AHEAD Karte darf man nicht auf einem nicht vorhandenen Feld landen (also nach dem Ziel).' when FieldType::HEDGEHOG return false, 'Durch Spielen der HURRY_AHEAD Karte darf man nicht auf einem Igelfeld landen.' when FieldType::SALAD return false, 'Spieler käme durch Spielen der HURRY_AHEAD Kart auf ein Salatfeld, hat aber keine Salate.' if player.salads < 1 when FieldType::HARE state2 = state.deep_clone state2.set_last_action(Card.new(CardType::HURRY_AHEAD)) state2.current_player.cards.delete(CardType::HURRY_AHEAD) return false, 'Spieler käme durch Spielen der HURRY_AHEAD Kart auf ein Hasenfeld, kann aber dann keine weitere Karte mehr spielen.' unless can_play_any_card(state2) when FieldType::GOAL return false, 'Spieler käme durch Spielen der HURRY_AHEAD Kart ins Ziel, darf es aber nicht betreten (entweder noch Salate oder mehr als 10 Karotten).' unless can_enter_goal(state) when FieldType::CARROT when FieldType::POSITION_1 when FieldType::POSITION_2 return true, '' when FieldType::START raise 'Player got onto start field by playing a hurry ahead card. This should never happen.' else raise "Unknown Type #{state.field(next_pos).type.inspect}" end return true, '' end # Überprüft ob der derzeitige Spieler die TAKE_OR_DROP_CARROTS Karte spielen darf. # @param state GameState # @param n 20 für nehmen, -20 für abgeben, 0 für nichts tun # @return [true, ''], falls die TAKE_OR_DROP_CARROTS Karte gespielt werden darf, [false, M], falls nicht, wobei M ein String mit dem Grund ist. def self.is_valid_to_play_take_or_drop_carrots(state, n) player = state.current_player return false, 'Spieler muss einen Vorwärtszug machen.' if player_must_advance(state) return false, 'Karten können nur auf Hasenfeldern gespielt werden.' unless state.current_field.type == FieldType::HARE return false, 'Spieler besitzt die Karte TAKE_OR_DROP_CARROTS nicht.' unless player.owns_card_of_type(CardType::TAKE_OR_DROP_CARROTS) return false, "#{n} ist keine erlaubte Anzahl beim Spielen der Karte TAKE_OR_DROP_CARROTS (erlaubt sind 20, -20 und 0)." unless [20, -20, 0].include?(n) return true, '' if n >= 0 # at this point, n has to be -20 return false, "Spieler hat keine 20 Karotten zum Abgeben (er hat #{player.carrots})." if player.carrots < 20 return true, '' end # Überprüft ob der derzeitige Spieler die EAT_SALAD Karte spielen darf. # @param state GameState # @return (true, ''), falls die EAT_SALAD Karte gespielt werden darf, (false, M) falls nicht, wobei M ein String mit dem Grund ist def self.is_valid_to_play_eat_salad(state) player = state.current_player return false, 'Es muss vorwärts gezogen werden.' if player_must_advance(state) return false, 'Karten können nur auf Hasenfeldern gespielt werden.' unless state.current_field.type == FieldType::HARE return false, 'Spieler besitzt die Karte nicht.' unless player.owns_card_of_type(CardType::EAT_SALAD) return false, 'Spieler hat keine Salate zum Essen' if player.salads < 1 return true, '' end # Überprüft ob der derzeitige Spieler irgendeine Karte spielen kann. # TAKE_OR_DROP_CARROTS wird nur mit 20 überprüft # @param state GameState # @return true, falls das Spielen einer Karte möglich ist def self.can_play_any_card(state) valid = false player = state.current_player player.cards.any? do |card| case card when CardType::EAT_SALAD is_valid_to_play_eat_salad(state)[0] when CardType::FALL_BACK is_valid_to_play_fall_back(state)[0] when CardType::HURRY_AHEAD is_valid_to_play_hurry_ahead(state)[0] when CardType::TAKE_OR_DROP_CARROTS is_valid_to_play_take_or_drop_carrots(state, 20)[0] else raise "Unknown CardType " + card end end end # Überprüft ob der derzeitige Spieler die Karte spielen kann. # @param state # @param c Karte die gespielt werden soll # @param n Parameter mit dem TAKE_OR_DROP_CARROTS überprüft wird, default 0 # @return true, falls das Spielen der entsprechenden karte möglich ist def self.is_valid_to_play_card(state, c, n = 0) case c when CardType::EAT_SALAD isValidToPlayEatSalad(state)[0] when CardType::FALL_BACK is_valid_to_play_fall_back(state)[0] when CardType::HURRY_AHEAD is_valid_to_play_hurry_ahead(state)[0] when CardType::TAKE_OR_DROP_CARROTS is_valid_to_play_take_or_drop_carrots(state, n)[0] else raise "Unknown CardType " + c end end def self.must_eat_salad(state) player = state.current_player # check whether player just moved to salad field and must eat salad field = state.board.field(player.index) if field.type == FieldType::SALAD if player.last_non_skip_action&.instance_of?(Advance) return true elsif player.last_non_skip_action&.instance_of?(Card) card_action = player.last_non_skip_action if card_action.card_type == CardType::FALL_BACK || card_action.card_type == CardType::HURRY_AHEAD return true end end end return false end # TODO difference isValidToPlayCard # @param state # @return def self.can_play_card(state) player = state.getCurrentPlayer() canPlayCard = state.getTypeAt(player.getFieldIndex()).equals(FieldType.HARE) player.getCards().each do |card| canPlayCard = canPlayCard || is_valid_to_play_card(state, card, 0) end return canPlayCard end # TODO difference isVAlidTOMove # @param state # @return def self.can_move(state) can_move = false max_distance = GameRules.calculate_movable_fields(state.getCurrentPlayer().getCarrots()) (1..max_distance).to_a.each do |i| can_move = can_move || isValidToAdvance(state, i) end return can_move end # Überprüft ob eine Karte gespielt werden muss. Sollte nach einem # Zug eines Spielers immer false sein, ansonsten ist Zug ungültig. # @param state derzeitiger GameState def self.must_play_card(state) state.current_player.must_play_card end # Überprüft ob ein der derzeitige Spieler das Ziel betreten darf # @param state GameState # @return def self.can_enter_goal(state) player = state.current_player player.carrots <= 10 && player.salads == 0 end end