software_challenge_client 0.3.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,101 +1,45 @@
1
- # encoding: UTF-8
1
+ # encoding: utf-8
2
2
  require_relative './util/constants'
3
3
  require_relative 'game_state'
4
4
  require_relative 'player'
5
5
  require_relative 'field_type'
6
6
  require_relative 'field'
7
7
 
8
- # A representation of a mississippi queen game board
8
+ # Ein Spielbrett bestehend aus 65 Feldern.
9
9
  class Board
10
-
11
10
  # @!attribute [r] fields
12
- # @note Better use {#field} to access fields.
13
- # @return [Hash<Field>] A field will be stored at the hash of the
14
- # coordinate-tuple (2-element-array) of the field.
11
+ # @note Besser über die {#field} Methode auf Felder zugreifen.
12
+ # @return [Array<Field>] Ein Feld wird an der Position entsprechend seines
13
+ # Index im Array gespeichert.
15
14
  attr_reader :fields
16
15
 
17
16
  # Initializes the board
18
17
  def initialize
19
- @fields = {}
18
+ @fields = []
20
19
  end
21
20
 
22
21
  def to_s
23
- fields.values.map { |f| f.type.key.to_s[0] }.join(' ')
22
+ fields.map { |f| f.type.value }.join(' ')
24
23
  end
25
24
 
26
25
  def ==(other)
27
- fields.each_with_index do |row, x|
28
- row.each_with_index do |field, y|
29
- return false if field != other.field(x, y)
30
- end
26
+ fields.each_with_index do |field, index|
27
+ return false if field != other.field(index)
31
28
  end
32
29
  true
33
30
  end
34
31
 
35
32
  def add_field(field)
36
- fields[[field.x, field.y]] = field
37
- end
38
-
39
- # @return [Integer, Integer] The coordinates of the neighbor of the field
40
- # specified by given coordinates in specified
41
- # direction.
42
- def get_neighbor(x, y, direction)
43
- directions = {
44
- even_row: {
45
- Direction::RIGHT.key=> [+1, 0],
46
- Direction::UP_RIGHT.key=>[+1, -1],
47
- Direction::UP_LEFT.key=>[0, -1],
48
- Direction::LEFT.key=>[-1, 0],
49
- Direction::DOWN_LEFT.key=>[0, +1],
50
- Direction::DOWN_RIGHT.key=>[+1, +1]
51
- },
52
- odd_row: {
53
- Direction::RIGHT.key=> [+1, 0],
54
- Direction::UP_RIGHT.key=> [ 0, -1],
55
- Direction::UP_LEFT.key=> [-1, -1],
56
- Direction::LEFT.key=> [-1, 0],
57
- Direction::DOWN_LEFT.key=> [-1, +1],
58
- Direction::DOWN_RIGHT.key=> [ 0, +1]
59
- }
60
- }
61
-
62
- parity = y.odd? ? :odd_row : :even_row
63
- dir = directions[parity][direction.key]
64
- return x + dir[0], y + dir[1]
65
- end
66
-
67
- # @return [Field] The field in given direction with given distance from the
68
- # field with given coordinates.
69
- def get_in_direction(from_x, from_y, direction, distance = 1)
70
- x = from_x
71
- y = from_y
72
- distance.times do
73
- x, y = get_neighbor(x, y, direction)
74
- end
75
- if !field(x, y).nil?
76
- return field(x, y)
77
- else
78
- raise FieldUnavailableException.new(x, y)
79
- end
80
- end
81
-
82
- # @return [Array<Field>] A list of fields in given direction up to given
83
- # distance from the field with given coordinates.
84
- # The start field is excluded.
85
- def get_all_in_direction(from_x, from_y, direction, distance = 1)
86
- (1..distance).to_a.map do |i|
87
- get_in_direction(
88
- from_x, from_y, direction, i
89
- )
90
- end
33
+ @fields[field.index] = field
91
34
  end
92
35
 
93
- # Access fields of the board.
36
+ # Zugriff auf die Felder des Spielfeldes
94
37
  #
95
- # @param x [Integer] The x-coordinate of the field.
96
- # @param y [Integer] The y-coordinate of the field.
97
- # @return [Field] the field at the given coordinates.
98
- def field(x, y)
99
- fields[[x,y]]
38
+ # @param index [Integer] Der Index des Feldes
39
+ # @return [Field] Das Feld mit dem gegebenen Index. Falls das Feld nicht
40
+ # exisitert (weil der Index ausserhalb von 0..64 liegt), wird ein neues
41
+ # Feld vom Typ INVALID zurückgegeben.
42
+ def field(index)
43
+ fields.fetch(index, Field.new(FieldType::INVALID, index))
100
44
  end
101
45
  end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ require 'typesafe_enum'
4
+ class CardType < TypesafeEnum::Base
5
+ # Nehme Karotten auf, oder leg sie ab
6
+ new :TAKE_OR_DROP_CARROTS
7
+ # Iß sofort einen Salat
8
+ new :EAT_SALAD
9
+ # Falle eine Position zurück
10
+ new :FALL_BACK
11
+ # Rücke eine Position vor
12
+ new :HURRY_AHEAD
13
+ end
@@ -1,65 +1,31 @@
1
1
  # encoding: UTF-8
2
2
  require_relative 'field_type'
3
3
 
4
- # A field on the game board.
4
+ # Ein Feld des Spielfelds. Ein Spielfeld ist durch den index eindeutig identifiziert.
5
+ # Das type Attribut gibt an, um welchen Feldtyp es sich handelt
5
6
  class Field
6
7
  # @!attribute [rw] type
7
- # @return [FieldType] the field's type
8
+ # @return [FieldType] der Typ des Feldes
8
9
  attr_accessor :type
9
- # @!attribute [r] x
10
- # @return [Integer] the field's x-coordinate
11
- attr_reader :x
12
- # @!attribute [r] y
13
- # @return [Integer] the field's y-coordinate
14
- attr_reader :y
15
-
16
- # @!attribute [r] direction
17
- # @return [Integer] the direction of the tile which the field belongs to
18
- attr_reader :direction
19
-
20
10
  # @!attribute [r] index
21
- # @return [Integer] the index of the tile which the field belongs to
11
+ # @return [Integer] der Index des Feldes (0 bis 64)
22
12
  attr_reader :index
23
13
 
24
- # @!attribute [r] points
25
- # @return [Integer] the points awarded to a player placed on this field additionally to points for current tile and passengers
26
- attr_reader :points
27
-
28
- # Initializer
14
+ # Konstruktor
29
15
  #
30
- # @param type [FieldType] field type
31
- # @param x [Integer] x-coordinate
32
- # @param y [Integer] y-coordinate
33
- # @param index [Integer] index of tile
34
- # @param direction [Integer] direction of tile
35
- # @param points [Integer] points
36
- def initialize(type, x, y, index, direction, points)
16
+ # @param type [FieldType] Feldtyp
17
+ # @param index [Integer] Index
18
+ def initialize(type, index)
37
19
  self.type = type
38
- @x = x
39
- @y = y
40
20
  @index = index
41
- @direction = direction
42
- @points = points
43
21
  end
44
22
 
45
23
  def ==(another_field)
46
24
  return self.type == another_field.type &&
47
- self.x == another_field.x &&
48
- self.y == another_field.y
49
- end
50
-
51
- # @return [Boolean] true if the field may never be moved onto.
52
- def blocked?
53
- [FieldType::BLOCKED,
54
- FieldType::PASSENGER0,
55
- FieldType::PASSENGER1,
56
- FieldType::PASSENGER2,
57
- FieldType::PASSENGER3,
58
- FieldType::PASSENGER4,
59
- FieldType::PASSENGER5].include? type
25
+ self.index == another_field.index
60
26
  end
61
27
 
62
28
  def to_s
63
- return "Field: x = #{self.x}, y = #{self.y}, type = #{self.type}"
29
+ return "Feld ##{self.index}, Typ = #{self.type}"
64
30
  end
65
31
  end
@@ -1,30 +1,24 @@
1
- # encoding: UTF-8
1
+ # encoding: utf-8
2
2
 
3
3
  require 'typesafe_enum'
4
- # All possible field types:
5
- # * WATER
6
- # * BLOCKED
7
- # * PASSENGER0
8
- # * PASSENGER1
9
- # * PASSENGER2
10
- # * PASSENGER3
11
- # * PASSENGER4
12
- # * PASSENGER5
13
- # * SANDBANK
14
- # * LOG
15
- # * GOAL
16
- # Access them with FieldType::WATER.
4
+ # Zahl- und Flaggenfelder Die veränderten Spielregeln sehen nur noch die
5
+ # Felder 1,2 vor. Die Positionsfelder 3 und 4 wurden in Möhrenfelder
6
+ # umgewandelt, und (1,5,6) sind jetzt Position-1-Felder.
17
7
  class FieldType < TypesafeEnum::Base
18
- new :WATER
19
- new :BLOCKED
20
- new :PASSENGER0
21
- new :PASSENGER1
22
- new :PASSENGER2
23
- new :PASSENGER3
24
- new :PASSENGER4
25
- new :PASSENGER5
26
- new :SANDBANK
27
- new :LOG
28
- new :GOAL
29
-
8
+ new :POSITION_1, '1'
9
+ new :POSITION_2, '2'
10
+ # Igelfeld
11
+ new :HEDGEHOG, 'I'
12
+ # Salatfeld
13
+ new :SALAD, 'S'
14
+ # Karottenfeld
15
+ new :CARROT, 'C'
16
+ # Hasenfeld
17
+ new :HARE, 'H'
18
+ # außerhalb des Spielfeldes
19
+ new :INVALID, 'X'
20
+ # Zielfeld
21
+ new :GOAL, 'G'
22
+ # Startfeld
23
+ new :START, '0'
30
24
  end
@@ -0,0 +1,378 @@
1
+ # coding: utf-8
2
+
3
+ require_relative 'field_type'
4
+ require_relative 'card_type'
5
+
6
+ # All methods which define the game rules. Needed for checking validity of moves
7
+ # and performing them.
8
+ class GameRules
9
+
10
+ # Berechnet wie viele Karotten für einen Zug der länge
11
+ # <code>moveCount</code> benötigt werden.
12
+ #
13
+ # @param moveCount Anzahl der Felder, um die bewegt wird
14
+ # @return Anzahl der benötigten Karotten
15
+ def self.calculate_carrots(moveCount)
16
+ (moveCount * (moveCount + 1)) / 2
17
+ end
18
+
19
+ # Berechnet, wieviele Züge mit <code>carrots</code> Karotten möglich sind.
20
+ #
21
+ # @param carrots maximal ausgegebene Karotten
22
+ # @return Felder um die maximal bewegt werden kann
23
+ def self.calculate_movable_fields(carrots)
24
+ # bei 30 Runden koennen nur 990 Karotten gesammelt werden
25
+ return 44 if carrots >= 990
26
+ return 0 if carrots < 1
27
+ (Math.sqrt(2 * carrots + 1/4) - 1/2).round
28
+ end
29
+
30
+ # Überprüft <code>Advance</code> Aktionen auf ihre Korrektheit. Folgende
31
+ # Spielregeln werden beachtet:
32
+ #
33
+ # - Der Spieler muss genügend Karotten für den Zug besitzen
34
+ # - Wenn das Ziel erreicht wird, darf der Spieler nach dem Zug maximal 10 Karotten übrig haben
35
+ # - Man darf nicht auf Igelfelder ziehen
36
+ # - Salatfelder dürfen nur betreten werden, wenn man noch Salate essen muss
37
+ # - Hasenfelder dürfen nur betreten werden, wenn man noch Hasenkarten ausspielen kann
38
+ #
39
+ # @param state GameState
40
+ # @param distance relativer Abstand zur aktuellen Position des Spielers
41
+ # @return [true, ''] falls ein Vorwärtszug möglich ist, [false, M] falls nicht, wobei M ein String mit einer Begruendung ist
42
+ def self.is_valid_to_advance(state, distance)
43
+ return false, 'Ein Vorwärtszug benötigt eine Mindestdistanz von einem Feld.' if distance <= 0
44
+ player = state.current_player
45
+ return false, 'Es muss ein Salat gegessen werden, bevor ein Vorwärtszug gemacht werden kann.' if must_eat_salad(state)
46
+ required_carrots = GameRules.calculate_carrots(distance)
47
+ return false, "Nicht genug Karotten, um #{distance} Felder vorwärts zu ziehen (Vorrat: #{player.carrots}, benötigt: #{required_carrots}" if (required_carrots > player.carrots)
48
+ new_position = player.index + distance
49
+ return false, 'Zielfeld wird von anderem Spieler besetzt.' if state.occupied_by_other_player?(state.field(new_position))
50
+ case state.field(new_position).type
51
+ when FieldType::INVALID
52
+ return false, "Zielfeld #{new_position} ist nicht vorhanden."
53
+ when FieldType::SALAD
54
+ return false, 'Ohne Salat darf ein Salatfeld nicht betreten werden.' if player.salads < 1
55
+ when FieldType::HARE
56
+ state2 = state.deep_clone
57
+ state2.set_last_action(Advance.new(distance))
58
+ state2.current_player.index = new_position
59
+ state2.current_player.carrots -= required_carrots
60
+ return false, 'Auf ein Hasenfeld darf nur gezogen werden, wenn eine Karte gespielt werden kann' unless can_play_any_card(state2)
61
+ when FieldType::GOAL
62
+ carrotsLeft = player.carrots - required_carrots
63
+ return false, "Auf das Zielfeld darf nur mit maximal 10 Karotten gezogen werden (es sind aber #{carrotsLeft} bei Erreichen des Zielfeldes)." unless carrotsLeft <= 10
64
+ return false, "Auf das Zielfeld darf nur ohne Salate gezogen werden (es sind aber #{player.salads} übrig)." unless player.salads == 0
65
+ when FieldType::HEDGEHOG
66
+ return false, 'Auf ein Igelfeld darf nicht vorwärts gezogen werden.'
67
+ when FieldType::CARROT, FieldType::POSITION_1, FieldType::START, FieldType::POSITION_2
68
+ return true, ''
69
+ else
70
+ raise "Unknown Type #{state.field(new_position).type.inspect}"
71
+ end
72
+ return true, ''
73
+ end
74
+
75
+ # Überprüft, ob ein Spieler aussetzen darf. Er darf dies, wenn kein anderer Zug möglich ist.
76
+ # @param state GameState
77
+ # @return true, falls der derzeitige Spieler keine andere Aktion machen kann.
78
+ def self.is_valid_to_skip(state)
79
+ return !GameRules.can_do_anything(state)
80
+ end
81
+
82
+ # Überprüft, ob ein Spieler einen Zug (keinen Aussetzug)
83
+ # @param state GameState
84
+ # @return true, falls ein Zug möglich ist.
85
+ def self.can_do_anything(state)
86
+ return can_play_any_card(state) || is_valid_to_fall_back(state)[0] ||
87
+ is_valid_to_exchange_carrots(state, 10)[0] ||
88
+ is_valid_to_exchange_carrots(state, -10)[0] ||
89
+ is_valid_to_eat(state)[0] || can_advance_to_any_field(state)
90
+ end
91
+
92
+ # Überprüft ob der derzeitige Spieler zu irgendeinem Feld einen Vorwärtszug machen kann.
93
+ # @param state GameState
94
+ # @return true, falls der Spieler irgendeinen Vorwärtszug machen kann
95
+ def self.can_advance_to_any_field(state)
96
+ fields = calculate_movable_fields(state.getCurrentPlayer().getCarrots())
97
+ (0..fields).to_a.any? do |i|
98
+ is_valid_to_advance(state, i)[0]
99
+ end
100
+ end
101
+
102
+ # Überprüft <code>EatSalad</code> Züge auf Korrektheit. Um einen Salat
103
+ # zu verzehren muss der Spieler sich:
104
+ #
105
+ # - auf einem Salatfeld befinden
106
+ # - noch mindestens einen Salat besitzen
107
+ # - vorher kein Salat auf diesem Feld verzehrt wurde
108
+ #
109
+ # @param state GameState
110
+ # @return [true, ''], falls ein Salad gegessen werden darf, [false, M] falls nicht, wobei M ein String mit dem Grund ist
111
+ def self.is_valid_to_eat(state)
112
+ return false, 'Salate dürfen nur auf Salatfeldern gegessen werden.' unless state.current_field.type == FieldType::SALAD
113
+ return false, 'Spieler hat keine Salate mehr zum Essen.' if state.current_player.salads < 1
114
+ return false, 'Spieler muss das Feld verlassen.' if player_must_advance(state)
115
+ return true, ''
116
+ end
117
+
118
+ # Überprüft ab der derzeitige Spieler im nächsten Zug einen Vorwärtszug machen muss.
119
+ # @param state GameState
120
+ # @return true, falls der derzeitige Spieler einen Vorwärtszug gemacht werden muss
121
+ def self.player_must_advance(state)
122
+ player = state.current_player
123
+ type = state.board.field(player.index).type
124
+
125
+ return true if (type == FieldType::HEDGEHOG || type == FieldType::START)
126
+
127
+ last_action = state.current_player.last_non_skip_action
128
+
129
+ if (!last_action.nil?)
130
+ if (last_action.instance_of? EatSalad)
131
+ return true
132
+ elsif (last_action.instance_of? Card)
133
+ # the player has to leave a rabbit field in next turn
134
+ if (last_action.type == CardType::EAT_SALAD)
135
+ return true
136
+ elsif (last_action.type == CardType::TAKE_OR_DROP_CARROTS) # the player has to leave the rabbit field
137
+ return true
138
+ end
139
+ end
140
+ end
141
+
142
+ return false
143
+ end
144
+
145
+ # Überprüft ob der derzeitige Spieler 10 Karotten nehmen oder abgeben kann.
146
+ # @param state GameState
147
+ # @param n 10 oder -10 je nach Fragestellung
148
+ # @return [true, ''], falls die durch n spezifizierte Aktion möglich ist, [false, M] falls nicht, wobei M ein String mit dem Grund ist
149
+ def self.is_valid_to_exchange_carrots(state, n)
150
+ player = state.current_player
151
+ return false, "Karotten können nur auf einem Karottenfeld #{n > 0 ? 'genommen' : 'abgegeben'} werden" if state.board.field(player.index).type != FieldType::CARROT
152
+ return true, '' if n == 10
153
+ return false, 'Gültige Karottenzahlen sind 10 und -10.' unless n == -10
154
+ return false, "Spieler hat keine 10 Karotten zum Abgeben (er hat #{player.carrots})." if player.carrots < 10
155
+ return true, ''
156
+ end
157
+
158
+ # Überprüft <code>FallBack</code> Züge auf Korrektheit
159
+ #
160
+ # @param state GameState
161
+ # @return [true, ''], falls der currentPlayer einen Rückzug machen darf, [false, M] falls nicht, wobei M ein String mit dem Grund ist.
162
+ def self.is_valid_to_fall_back(state)
163
+ return false, 'Spieler muss einen Salat fressen.' if must_eat_salad(state)
164
+ target_field = state.previous_field_of_type(
165
+ FieldType::HEDGEHOG, state.current_player.index
166
+ )
167
+ return false, 'Es gibt kein Igelfeld hinter dem Spieler.' if target_field.nil?
168
+ return false, 'Das Igelfeld hinter dem Spieler ist besetzt.' if state.occupied_by_other_player?(target_field)
169
+ return true, ''
170
+ end
171
+
172
+ # Überprüft ob der derzeitige Spieler die <code>FALL_BACK</code> Karte spielen darf.
173
+ # @param state GameState
174
+ # @return [true, ''], falls die <code>FALL_BACK</code> Karte gespielt werden darf, [false, M] falls nicht, wobei M ein String mit dem Grund ist.
175
+ def self.is_valid_to_play_fall_back(state)
176
+ player = state.current_player
177
+ return false, 'Spieler muss einen Vorwärtszug machen.' if player_must_advance(state)
178
+ return false, 'Karten können nur auf Hasenfeldern gespielt werden.' unless state.current_field.type == FieldType::HARE
179
+ return false, 'Nur der erste Spieler darf die FALL_BACK Karte spielen.' unless state.is_first(player)
180
+ return false, 'Spieler besitzt die Karte FALL_BACK nicht.' unless player.owns_card_of_type(CardType::FALL_BACK)
181
+
182
+ next_pos = state.other_player.index - 1
183
+
184
+ case state.field(next_pos).type
185
+ when FieldType::INVALID
186
+ return false, 'Durch Spielen der FALL_BACK Karte darf man nicht auf einem nicht vorhandenen Feld landen (also vor dem Start).'
187
+ when FieldType::HEDGEHOG
188
+ return false, 'Durch Spielen der FALL_BACK Karte darf man nicht auf einem Igelfeld landen.'
189
+ when FieldType::SALAD
190
+ return false, 'Spieler käme durch Spielen der FALL_BACK Kart auf ein Salatfeld, hat aber keine Salate.' if player.salads < 1
191
+ when FieldType::HARE
192
+ state2 = state.deep_clone
193
+ state2.set_last_action(Card.new(CardType::HURRY_AHEAD))
194
+ state2.current_player.cards.delete(CardType::FALL_BACK)
195
+ 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)
196
+ when FieldType::START
197
+ when FieldType::CARROT
198
+ when FieldType::POSITION_1
199
+ when FieldType::POSITION_2
200
+ return true, ''
201
+ when FieldType::GOAL
202
+ raise 'Player got onto goal by playing a fall back card. This should never happen.'
203
+ else
204
+ raise "Unknown Type #{state.field(next_pos).type.inspect}"
205
+ end
206
+ return true, ''
207
+ end
208
+
209
+ # Überprüft ob der derzeitige Spieler die <code>HURRY_AHEAD</code> Karte spielen darf.
210
+ # @param state GameState
211
+ # @return [true, ''], falls die <code>HURRY_AHEAD</code> Karte gespielt werden darf, [false, M], falls nicht, wobei M ein String mit dem Grund ist.
212
+ def self.is_valid_to_play_hurry_ahead(state)
213
+ player = state.current_player
214
+ return false, 'Spieler muss einen Vorwärtszug machen.' if player_must_advance(state)
215
+ return false, 'Karten können nur auf Hasenfeldern gespielt werden.' unless state.current_field.type == FieldType::HARE
216
+ return false, 'Nur der zweite Spieler darf die HURRY_AHEAD Karte spielen.' unless state.is_second(player)
217
+ return false, 'Spieler besitzt die Karte HURRY_AHEAD nicht.' unless player.owns_card_of_type(CardType::HURRY_AHEAD)
218
+
219
+ o = state.other_player
220
+ next_pos = o.index + 1
221
+
222
+ case state.field(next_pos).type
223
+ when FieldType::INVALID
224
+ return false, 'Durch Spielen der HURRY_AHEAD Karte darf man nicht auf einem nicht vorhandenen Feld landen (also nach dem Ziel).'
225
+ when FieldType::HEDGEHOG
226
+ return false, 'Durch Spielen der HURRY_AHEAD Karte darf man nicht auf einem Igelfeld landen.'
227
+ when FieldType::SALAD
228
+ return false, 'Spieler käme durch Spielen der HURRY_AHEAD Kart auf ein Salatfeld, hat aber keine Salate.' if player.salads < 1
229
+ when FieldType::HARE
230
+ state2 = state.deep_clone
231
+ state2.set_last_action(Card.new(CardType::HURRY_AHEAD))
232
+ state2.current_player.cards.delete(CardType::HURRY_AHEAD)
233
+ 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)
234
+ when FieldType::GOAL
235
+ 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)
236
+ when FieldType::CARROT
237
+ when FieldType::POSITION_1
238
+ when FieldType::POSITION_2
239
+ return true, ''
240
+ when FieldType::START
241
+ raise 'Player got onto start field by playing a hurry ahead card. This should never happen.'
242
+ else
243
+ raise "Unknown Type #{state.field(next_pos).type.inspect}"
244
+ end
245
+ return true, ''
246
+ end
247
+
248
+ # Überprüft ob der derzeitige Spieler die <code>TAKE_OR_DROP_CARROTS</code> Karte spielen darf.
249
+ # @param state GameState
250
+ # @param n 20 für nehmen, -20 für abgeben, 0 für nichts tun
251
+ # @return [true, ''], falls die <code>TAKE_OR_DROP_CARROTS</code> Karte gespielt werden darf, [false, M], falls nicht, wobei M ein String mit dem Grund ist.
252
+ def self.is_valid_to_play_take_or_drop_carrots(state, n)
253
+ player = state.current_player
254
+ return false, 'Spieler muss einen Vorwärtszug machen.' if player_must_advance(state)
255
+ return false, 'Karten können nur auf Hasenfeldern gespielt werden.' unless state.current_field.type == FieldType::HARE
256
+ return false, 'Spieler besitzt die Karte TAKE_OR_DROP_CARROTS nicht.' unless player.owns_card_of_type(CardType::TAKE_OR_DROP_CARROTS)
257
+ 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)
258
+ return true, '' if n >= 0
259
+ # at this point, n has to be -20
260
+ return false, "Spieler hat keine 20 Karotten zum Abgeben (er hat #{player.carrots})." if player.carrots < 20
261
+ return true, ''
262
+ end
263
+
264
+ # Überprüft ob der derzeitige Spieler die <code>EAT_SALAD</code> Karte spielen darf.
265
+ # @param state GameState
266
+ # @return (true, ''), falls die <code>EAT_SALAD</code> Karte gespielt werden darf, (false, M) falls nicht, wobei M ein String mit dem Grund ist
267
+ def self.is_valid_to_play_eat_salad(state)
268
+ player = state.current_player
269
+ return false, 'Es muss vorwärts gezogen werden.' if player_must_advance(state)
270
+ return false, 'Karten können nur auf Hasenfeldern gespielt werden.' unless state.current_field.type == FieldType::HARE
271
+ return false, 'Spieler besitzt die Karte nicht.' unless player.owns_card_of_type(CardType::EAT_SALAD)
272
+ return false, 'Spieler hat keine Salate zum Essen' if player.salads < 1
273
+ return true, ''
274
+ end
275
+
276
+ # Überprüft ob der derzeitige Spieler irgendeine Karte spielen kann.
277
+ # TAKE_OR_DROP_CARROTS wird nur mit 20 überprüft
278
+ # @param state GameState
279
+ # @return true, falls das Spielen einer Karte möglich ist
280
+ def self.can_play_any_card(state)
281
+ valid = false
282
+ player = state.current_player
283
+
284
+ player.cards.any? do |card|
285
+ case card
286
+ when CardType::EAT_SALAD
287
+ is_valid_to_play_eat_salad(state)[0]
288
+ when CardType::FALL_BACK
289
+ is_valid_to_play_fall_back(state)[0]
290
+ when CardType::HURRY_AHEAD
291
+ is_valid_to_play_hurry_ahead(state)[0]
292
+ when CardType::TAKE_OR_DROP_CARROTS
293
+ is_valid_to_play_take_or_drop_carrots(state, 20)[0]
294
+ else
295
+ raise "Unknown CardType " + card
296
+ end
297
+ end
298
+ end
299
+
300
+ # Überprüft ob der derzeitige Spieler die Karte spielen kann.
301
+ # @param state
302
+ # @param c Karte die gespielt werden soll
303
+ # @param n Parameter mit dem TAKE_OR_DROP_CARROTS überprüft wird, default 0
304
+ # @return true, falls das Spielen der entsprechenden karte möglich ist
305
+ def self.is_valid_to_play_card(state, c, n = 0)
306
+ case c
307
+ when CardType::EAT_SALAD
308
+ isValidToPlayEatSalad(state)[0]
309
+ when CardType::FALL_BACK
310
+ is_valid_to_play_fall_back(state)[0]
311
+ when CardType::HURRY_AHEAD
312
+ is_valid_to_play_hurry_ahead(state)[0]
313
+ when CardType::TAKE_OR_DROP_CARROTS
314
+ is_valid_to_play_take_or_drop_carrots(state, n)[0]
315
+ else
316
+ raise "Unknown CardType " + c
317
+ end
318
+ end
319
+
320
+ def self.must_eat_salad(state)
321
+ player = state.current_player
322
+ # check whether player just moved to salad field and must eat salad
323
+ field = state.board.field(player.index)
324
+ if field.type == FieldType::SALAD
325
+ if player.last_non_skip_action&.instance_of?(Advance)
326
+ return true
327
+ elsif player.last_non_skip_action&.instance_of?(Card)
328
+ card_action = player.last_non_skip_action
329
+ if card_action.card_type == CardType::FALL_BACK ||
330
+ card_action.card_type == CardType::HURRY_AHEAD
331
+ return true
332
+ end
333
+ end
334
+ end
335
+ return false
336
+ end
337
+
338
+ # TODO difference isValidToPlayCard
339
+ # @param state
340
+ # @return
341
+ def self.can_play_card(state)
342
+ player = state.getCurrentPlayer()
343
+ canPlayCard = state.getTypeAt(player.getFieldIndex()).equals(FieldType.HARE)
344
+ player.getCards().each do |card|
345
+ canPlayCard = canPlayCard || is_valid_to_play_card(state, card, 0)
346
+ end
347
+ return canPlayCard
348
+ end
349
+
350
+ # TODO difference isVAlidTOMove
351
+ # @param state
352
+ # @return
353
+ def self.can_move(state)
354
+ can_move = false
355
+ max_distance = GameRules.calculate_movable_fields(state.getCurrentPlayer().getCarrots())
356
+ (1..max_distance).to_a.each do |i|
357
+ can_move = can_move || isValidToAdvance(state, i)
358
+ end
359
+ return can_move
360
+ end
361
+
362
+ # Überprüft ob eine Karte gespielt werden muss. Sollte nach einem
363
+ # Zug eines Spielers immer false sein, ansonsten ist Zug ungültig.
364
+ # @param state derzeitiger GameState
365
+ def self.must_play_card(state)
366
+ state.current_player.must_play_card
367
+ end
368
+
369
+
370
+ # Überprüft ob ein der derzeitige Spieler das Ziel betreten darf
371
+ # @param state GameState
372
+ # @return
373
+ def self.can_enter_goal(state)
374
+ player = state.current_player
375
+ player.carrots <= 10 && player.salads == 0
376
+ end
377
+
378
+ end