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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2e078c2ea342feb8da789442c4ad6cdd3706ff15
4
- data.tar.gz: 90bcc5974f4a1388655101874062157a4f817dcd
3
+ metadata.gz: 4d55ebfd85d0ed449a11437fd91481c0b7b6f2e1
4
+ data.tar.gz: 027eadb37b996d8e07ad680db5aa97807d661051
5
5
  SHA512:
6
- metadata.gz: d673b541408a816e100fe286d370d661ac7991235049d610ecf855426c22f2548a425f0f77f82214db1c790e5b089b35bacdc5a3c083bbd85b91b0f2180d5182
7
- data.tar.gz: ff79a54d09798ce8222711ace8b15db2e8e09ece78d27b4f7278ee21e4b4274a735819ee55677ed8266881b3894f327f8f2134222c4f71f0211b63816117e0c7
6
+ metadata.gz: c78a79ba1d86132c4abb89c489a49480b2a71d14449edc83e11d2eba20ea3bc5bdce3deaddc2d685ff63673631df65c6efd9f4ffdbb3f7638a5dc2ee12736990
7
+ data.tar.gz: b70311d5c10252ce0a92c78a3e0f1623d055a93d9b1a6b9bdd39fbc2b0d60a29c7c39261bd54edf41af2707b7838884a5b02331c53e23a18eff42fbf4713b248
data/.rubocop.yml CHANGED
@@ -1,6 +1,8 @@
1
1
  AllCops:
2
+ TargetRubyVersion: 2.4
2
3
  DefaultFormatter: fuubar
3
4
  DisplayStyleGuide: true
4
5
  DisplayCopNames: false
5
- TargetRubyVersion: 2.0
6
6
  #require: rubocop-rspec
7
+ Style/AsciiComments:
8
+ Enabled: false # we use german comments to document relevant methods for the pupils
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.4.1
data/Guardfile CHANGED
@@ -36,9 +36,9 @@ group :red_green_refactor, halt_on_fail: true do
36
36
  watch('spec/spec_helper.rb') { 'spec' }
37
37
  end
38
38
 
39
- guard :rubocop, all_on_start: false do
40
- # This never includes external ruby files because of the
41
- # directory constraint at the top of this file:
42
- watch(/.*.rb/)
43
- end
39
+ # guard :rubocop, all_on_start: false do
40
+ # # This never includes external ruby files because of the
41
+ # # directory constraint at the top of this file:
42
+ # watch(/.*.rb/)
43
+ # end
44
44
  end
data/RELEASES.md CHANGED
@@ -1,5 +1,11 @@
1
+ = 1.0.0
2
+
3
+ First version for game "Hase und Igel".
4
+
1
5
  = 0.3.4
2
6
 
7
+ Last version for game "Mississippi Queen".
8
+
3
9
  - Renamed Turn#direction to Turn#turn_steps to make clearer that a number should be given, not a Direction instance.
4
10
  - Corrected generation of XML for Push-actions
5
11
 
data/example/client.rb CHANGED
@@ -8,6 +8,9 @@ class Client < ClientInterface
8
8
 
9
9
  attr_accessor :gamestate
10
10
 
11
+ # Anzahl der Spielfelder
12
+ NUM_FIELDS = 65
13
+
11
14
  def initialize(log_level)
12
15
  logger.level = log_level
13
16
  logger.info 'Einfacher Spieler wurde erstellt.'
@@ -16,44 +19,79 @@ class Client < ClientInterface
16
19
  # gets called, when it's your turn
17
20
  def move_requested
18
21
  logger.info "Spielstand: #{gamestate.points_for_player(gamestate.current_player)} - #{gamestate.points_for_player(gamestate.other_player)}"
19
- mov = best_move
20
- logger.debug "Zug gefunden: #{mov}" unless mov.nil?
21
- mov
22
+ move = best_move
23
+ logger.debug "Zug gefunden: #{move}" unless move.nil?
24
+ move
22
25
  end
23
26
 
24
- # choose a move giving the most points
25
27
  def best_move
26
- # try all moves in all directions
27
- best = nil
28
- points_for_best = 0
29
- Direction.each do |direction|
30
- [1, 2].each do |speed|
31
- move = Move.new
32
- if gamestate.current_player.velocity != speed
33
- move.add_action(Acceleration.new(speed - gamestate.current_player.velocity))
34
- end
35
- # turn in that direction
36
- possible_turn = Direction.from_to(gamestate.current_player.direction, direction)
37
- if possible_turn.direction != 0
38
- move.add_action(possible_turn)
39
- end
40
- move.add_action(Advance.new(speed))
41
- gamestate_copy = gamestate.deep_clone
42
- begin
43
- logger.debug("Teste Zug #{move} auf gueltigkeit")
44
- move.perform!(gamestate_copy, gamestate_copy.current_player)
45
- points_for_move = gamestate_copy.points_for_player(gamestate_copy.current_player)
46
- logger.debug("Zug #{move} gueltig und wuerde #{points_for_move} Punkte geben!.")
47
- on_sandbank = gamestate_copy.board.field(gamestate_copy.current_player.x, gamestate_copy.current_player.y).type == FieldType::SANDBANK
48
- if !on_sandbank && (best.nil? || points_for_move > points_for_best)
49
- best = move
50
- points_for_best = points_for_move
28
+ possible_moves = gamestate.possible_moves # Enthält mindestens ein Element
29
+ salad_moves = []
30
+ winning_moves = []
31
+ selected_moves = []
32
+
33
+ index = gamestate.current_player.index
34
+ possible_moves.each do |move|
35
+ move.actions.each do |action|
36
+ case action.type
37
+ when :advance
38
+ target_field_index = action.distance + index
39
+ if target_field_index == NUM_FIELDS - 1
40
+ winning_moves << move
41
+ elsif gamestate.field(target_field_index).type == FieldType::SALAD
42
+ salad_moves << move
43
+ else
44
+ selected_moves << move
45
+ end
46
+ when :card
47
+ if action.card_type == CardType::EAT_SALAD
48
+ # Zug auf Hasenfeld und danach Salatkarte
49
+ salad_moves << move
50
+ # Muss nicht zusätzlich ausgewählt werden, wurde schon durch Advance ausgewählt
51
+ end
52
+ when :exchange_carrots
53
+ if action.value == 10 &&
54
+ gamestate.current_player.carrots < 30 &&
55
+ index < 40 &&
56
+ !gamestate.current_player.last_non_skip_action.instance_of?(ExchangeCarrots)
57
+ # Nehme nur Karotten auf, wenn weniger als 30 und nur am Anfang und nicht zwei
58
+ # mal hintereinander
59
+ selected_moves << move
60
+ elsif action.value == -10 &&
61
+ gamestate.current_player.carrots > 30 &&
62
+ index >= 40
63
+ # Abgeben von Karotten ist nur am Ende sinnvoll
64
+ selected_moves << move
65
+ end
66
+ when :fall_back
67
+ if index > 56 && # letztes Salatfeld
68
+ gamestate.current_player.salads > 0
69
+ # Falle nur am Ende (index > 56) zurück, außer du musst noch einen Salat loswerden
70
+ selected_moves << move
71
+ elsif index <= 56 &&
72
+ gamestate.previous_field_of_type(FieldType::HEDGEHOG, index) &&
73
+ index - gamestate.previous_field_of_type(FieldType::HEDGEHOG, index).index < 5
74
+ # Falle zurück, falls sich Rückzug lohnt (nicht zu viele Karotten aufnehmen)
75
+ selected_moves << move
76
+ end
77
+ else
78
+ # Füge Salatessen oder Skip hinzu
79
+ selected_moves << move
51
80
  end
52
- rescue InvalidMoveException => e
53
- logger.debug("Zug #{move} ist ungueltig: #{e}")
54
- end
55
81
  end
56
82
  end
57
- best
83
+
84
+ if !winning_moves.empty?
85
+ logger.info("Waehle Gewinnzug")
86
+ winning_moves.sample
87
+ elsif !salad_moves.empty?
88
+ # es gibt die Möglichkeit einen Salat zu essen
89
+ logger.info("Waehle Zug zum Salatessen")
90
+ salad_moves.sample
91
+ elsif !selected_moves.empty?
92
+ selected_moves.sample
93
+ else
94
+ possible_moves.sample
95
+ end
58
96
  end
59
97
  end
@@ -17,5 +17,5 @@ module SoftwareChallengeClient
17
17
  require 'software_challenge_client/player_color'
18
18
  require 'software_challenge_client/protocol'
19
19
  require 'software_challenge_client/runner'
20
- require 'software_challenge_client/direction'
20
+ require 'software_challenge_client/game_rules'
21
21
  end
@@ -13,7 +13,7 @@ class Action
13
13
  raise 'must be overridden'
14
14
  end
15
15
 
16
- def perform!(_gamestate, _current_player)
16
+ def perform!(_gamestate)
17
17
  raise 'must be overridden'
18
18
  end
19
19
 
@@ -28,251 +28,171 @@ class Action
28
28
  end
29
29
  end
30
30
 
31
- # Accelerate by {#acceleration}. To decelerate, use a negative value.
32
- class Acceleration < Action
33
- attr_reader :acceleration
31
+ # Ein Vorwärtszug, um spezifizierte Distanz. Verbrauchte Karroten werden mit k =
32
+ # (distance * (distance + 1)) / 2 berechnet (Gaußsche Summe)
33
+ class Advance < Action
34
+ attr_reader :distance
34
35
 
35
- def initialize(acceleration)
36
- @acceleration = acceleration
36
+ def initialize(distance)
37
+ @distance = distance
37
38
  end
38
39
 
39
40
  # Perform the action.
40
41
  #
41
- # @param gamestate [GameState] The game state on which the action will be performed. Performing may change the game state.
42
- # @param current_player [Player] The player for which the action will be performed.
43
- def perform!(gamestate, current_player)
44
- new_velocity = current_player.velocity + acceleration
45
- if new_velocity < 1
46
- invalid 'Geschwindigkeit darf nicht unter 1 verringert werden.'
47
- end
48
- if new_velocity > 6
49
- invalid 'Geschwindigkeit darf nicht über 6 erhöht werden.'
50
- end
51
- acceleration.abs.times do
52
- if gamestate.free_acceleration?
53
- gamestate.free_acceleration = false
54
- elsif current_player.coal.zero?
55
- invalid 'Nicht genug Kohle zum Beschleunigen.'
56
- else
57
- current_player.coal -= 1
58
- end
42
+ # @param gamestate [GameState] The game state on which the action will be
43
+ # performed. Performing may change the game state. The action is performed for
44
+ # the current player of the game state.
45
+ def perform!(gamestate)
46
+ valid, message = GameRules.is_valid_to_advance(gamestate, distance)
47
+ invalid(message) unless valid
48
+ # perform state changes
49
+ required_carrots = distance * (distance + 1) / 2
50
+ gamestate.current_player.carrots -= required_carrots
51
+ gamestate.current_player.index += distance
52
+ if gamestate.current_field.type == FieldType::HARE
53
+ gamestate.current_player.must_play_card = true
59
54
  end
60
- if gamestate.board.field(current_player.x, current_player.y).type == FieldType::SANDBANK
61
- invalid 'Auf einer Sandbank kann nicht beschleunigt werden.'
62
- end
63
- current_player.velocity = new_velocity
64
- # This works only when acceleration is the first action in a move. The move
65
- # class has to check that.
66
- current_player.movement = new_velocity
67
55
  end
68
56
 
69
57
  def type
70
- :acceleration
58
+ :advance
71
59
  end
72
60
 
73
61
  def ==(other)
74
- other.type == type && other.acceleration == acceleration
62
+ other.type == type && other.distance == distance
75
63
  end
76
64
  end
77
65
 
78
- # Turn by {#turn_steps}.
79
- class Turn < Action
80
- # Number of steps to turn. Negative values for turning clockwise, positive for
81
- # counterclockwise.
82
- attr_reader :turn_steps
83
-
84
- def initialize(turn_steps)
85
- @turn_steps = turn_steps
86
- end
87
-
88
- # (see Acceleration#perform!)
89
- def perform!(gamestate, current_player)
90
- invalid 'Drehung um 0 ist ungültig' if turn_steps.zero?
91
- if gamestate
92
- .board
93
- .field(current_player.x, current_player.y)
94
- .type == FieldType::SANDBANK
95
- invalid 'Drehung auf Sandbank nicht erlaubt'
96
- end
97
- needed_coal = turn_steps.abs
98
- needed_coal -= 1 if gamestate.free_turn?
99
- if needed_coal > 0 && gamestate.additional_free_turn_after_push?
100
- needed_coal -= 1
101
- gamestate.additional_free_turn_after_push = false
102
- end
103
- if needed_coal > current_player.coal
104
- invalid "Nicht genug Kohle für Drehung um #{turn_steps}. "\
105
- "Habe #{current_player.coal}, brauche #{needed_coal}."
66
+ # Play a card.
67
+ class Card < Action
68
+ # only for type TAKE_OR_DROP_CARROTS
69
+ attr_reader :value
70
+
71
+ attr_reader :card_type
72
+
73
+ def initialize(card_type, value = 0)
74
+ @card_type = card_type
75
+ @value = value
76
+ end
77
+
78
+ # (see Advance#perform!)
79
+ def perform!(gamestate)
80
+ gamestate.current_player.must_play_card = false
81
+ case card_type
82
+ when CardType::EAT_SALAD
83
+ valid, message = GameRules.is_valid_to_play_eat_salad(gamestate)
84
+ invalid("Das Ausspielen der EAT_SALAD Karte ist nicht möglich. " + message) unless valid
85
+ gamestate.current_player.salads -= 1
86
+ if gamestate.is_first(gamestate.current_player)
87
+ gamestate.current_player.carrots += 10
88
+ else
89
+ gamestate.current_player.carrots += 20
90
+ end
91
+ when CardType::FALL_BACK
92
+ valid, message = GameRules.is_valid_to_play_fall_back(gamestate)
93
+ invalid("Das Ausspielen der FALL_BACK Karte ist nicht möglich. " + message) unless valid
94
+ gamestate.current_player.index = gamestate.other_player.index - 1
95
+ if gamestate.field(gamestate.current_player.index).type == FieldType::HARE
96
+ gamestate.current_player.must_play_card = true
97
+ end
98
+ when CardType::HURRY_AHEAD
99
+ valid, message = GameRules.is_valid_to_play_hurry_ahead(gamestate)
100
+ invalid("Das Ausspielen der HURRY_AHEAD Karte ist nicht möglich. " + message) unless valid
101
+ gamestate.current_player.index = gamestate.other_player.index + 1
102
+ if gamestate.field(gamestate.current_player.index).type == FieldType::HARE
103
+ gamestate.current_player.must_play_card = true
104
+ end
105
+ when CardType::TAKE_OR_DROP_CARROTS
106
+ valid, message = GameRules.is_valid_to_play_take_or_drop_carrots(gamestate, value)
107
+ invalid("Das Ausspielen der TAKE_OR_DROP_CARROTS Karte ist nicht möglich. " + message) unless valid
108
+ gamestate.current_player.carrots += value
109
+ else
110
+ raise "Unknown card type #{card_type.inspect}!"
106
111
  end
107
-
108
- current_player.direction =
109
- Direction.get_turn_direction(current_player.direction, turn_steps)
110
- current_player.coal -= [0, needed_coal].max
111
- gamestate.free_turn = false
112
+ gamestate.set_last_action(self)
113
+ gamestate.current_player.cards.delete(self.type)
112
114
  end
113
115
 
114
116
  def type
115
- :turn
117
+ :card
116
118
  end
117
119
 
118
120
  def ==(other)
119
- other.type == type && other.turn_steps == turn_steps
121
+ other.card_type == card_type &&
122
+ (card_type != CardType::TAKE_OR_DROP_CARROTS || (other.value == value))
120
123
  end
121
124
  end
122
125
 
123
- # Go forward in the current direction by {#distance}. When on a sandbank, a
124
- # value of -1 to go backwards is also legal.
125
- class Advance < Action
126
- attr_reader :distance
127
-
128
- def initialize(distance)
129
- @distance = distance
126
+ # Ein Aussetzzug. Ist nur erlaubt, sollten keine anderen Züge möglich sei
127
+ class Skip < Action
128
+ def initialize()
130
129
  end
131
130
 
132
- # (see Acceleration#perform!)
133
- def perform!(gamestate, current_player)
134
- invalid 'Bewegung um 0 ist unzulässig.' if distance.zero?
135
- if distance < 0 && gamestate.board.field(current_player.x, current_player.y).type != FieldType::SANDBANK
136
- invalid 'Negative Bewegung ist nur auf Sandbank erlaubt.'
137
- end
138
- begin
139
- fields = gamestate.board.get_all_in_direction(
140
- current_player.x, current_player.y, current_player.direction, distance
141
- )
142
- rescue FieldUnavailableException => e
143
- invalid "Feld (#{e.x}, #{e.y}) ist nicht vorhanden"
144
- end
145
- # test if all fields are passable
146
- if fields.any?(&:blocked?)
147
- invalid 'Der Weg ist blockiert.'
148
- end
149
- # Test if movement is enough.
150
- req_movement = required_movement(gamestate, current_player)
151
- if req_movement > current_player.movement
152
- invalid 'Nicht genug Bewegungspunkte.'
153
- end
154
- # test if opponent is not on fields over which is moved
155
- if fields[0...-1].any? { |f| gamestate.occupied_by_other_player? f }
156
- invalid 'Man darf nicht über den Gegner fahren.'
157
- end
158
- # test if moving over sandbank
159
- if fields[0...-1].any? { |f| f.type == FieldType::SANDBANK }
160
- invalid 'Die Bewegung darf nur auf einer Sandbank enden, '\
161
- 'nicht über sie hinaus gehen.'
162
- end
163
- target_field = fields.last
164
- current_player.x = target_field.x
165
- current_player.y = target_field.y
166
-
167
- if target_field.type == FieldType::SANDBANK
168
- current_player.movement = 0
169
- current_player.velocity = 1
170
- else
171
- current_player.movement -= req_movement
172
- end
173
-
174
- # test for passenger
175
- if current_player.velocity == 1
176
- required_field_for_direction = {
177
- Direction::RIGHT.key=> FieldType::PASSENGER3.key,
178
- Direction::UP_RIGHT.key=> FieldType::PASSENGER4.key,
179
- Direction::UP_LEFT.key=> FieldType::PASSENGER5.key,
180
- Direction::LEFT.key=> FieldType::PASSENGER0.key,
181
- Direction::DOWN_LEFT.key=> FieldType::PASSENGER2.key,
182
- Direction::DOWN_RIGHT.key=> FieldType::PASSENGER1.key
183
- }
184
- Direction.each do |direction|
185
- begin
186
- neighbor = gamestate.board.get_in_direction(current_player.x, current_player.y, direction)
187
- if neighbor.type.key == required_field_for_direction[direction.key]
188
- if current_player.passengers < 2
189
- current_player.passengers += 1
190
- neighbor.type = FieldType::BLOCKED
191
- end
192
- end
193
- rescue FieldUnavailableException
194
- # neighbor did not exist, that is okay
195
- end
196
- end
197
- end
131
+ def type
132
+ :skip
133
+ end
198
134
 
135
+ def ==(other)
136
+ other.type == type
199
137
  end
138
+ end
200
139
 
201
- # returns the required movement points to perform this action
202
- def required_movement(gamestate, current_player)
203
- gamestate.board.get_all_in_direction(current_player.x, current_player.y, current_player.direction, distance).map do |field|
204
- # pushing costs one more movement
205
- on_opponent = field.x == gamestate.other_player.x && field.y == gamestate.other_player.y
206
- case field.type
207
- when FieldType::WATER, FieldType::GOAL, FieldType::SANDBANK
208
- on_opponent ? 2 : 1
209
- when FieldType::LOG
210
- on_opponent ? 3 : 2
211
- end
212
- end.reduce(:+)
140
+ # Eine Salatessen-Aktion. Kann nur auf einem Salatfeld ausgeführt werden. Muss ausgeführt werden,
141
+ # ein Salatfeld betreten wird. Nachdem die Aktion ausgefürht wurde, muss das Salatfeld verlassen
142
+ # werden, oder es muss ausgesetzt werden.
143
+ # Duch eine Salatessen-Aktion wird ein Salat verbraucht und es werden je nachdem ob der Spieler führt
144
+ # oder nicht 10 oder 30 Karotten aufgenommen.
145
+ class EatSalad < Action
146
+ def initialize()
213
147
  end
214
148
 
215
149
  def type
216
- :advance
150
+ :eat_salad
217
151
  end
218
152
 
219
153
  def ==(other)
220
- other.type == type && other.distance == distance
154
+ other.type == type
221
155
  end
222
156
  end
223
157
 
224
- # Push the opponent in {#direction}
225
- class Push < Action
226
- # @return [Direction] the direction where to push.
227
- attr_reader :direction
158
+ # Karottentauschaktion. Es können auf einem Karottenfeld 10 Karotten abgegeben oder aufgenommen werden.
159
+ # Dies kann beliebig oft hintereinander ausgeführt werden.
160
+ class ExchangeCarrots < Action
161
+ attr_accessor :value
228
162
 
229
- # @param direction [Direction]
230
- def initialize(direction)
231
- @direction = direction
163
+ def initialize(value)
164
+ @value = value
232
165
  end
233
166
 
234
- # (see Acceleration#perform!)
235
- def perform!(gamestate, current_player)
236
- if gamestate.other_player.x != current_player.x ||
237
- gamestate.other_player.y != current_player.y
238
- invalid 'Abdrängen ist nur auf dem Feld des Gegners möglich.'
239
- end
240
- other_player_field =
241
- gamestate.board.field(gamestate.other_player.x, gamestate.other_player.y)
242
- if other_player_field.type == FieldType::SANDBANK
243
- invalid 'Abdrängen von einer Sandbank ist nicht erlaubt.'
244
- end
245
- if direction == Direction.get_turn_direction(current_player.direction, 3)
246
- invalid 'Man darf nicht hinter sich abdrängen.'
247
- end
248
-
249
- target_x, target_y =
250
- gamestate.board.get_neighbor(
251
- gamestate.other_player.x,
252
- gamestate.other_player.y,
253
- direction
254
- )
167
+ def perform!(gamestate)
168
+ valid, message = GameRules.is_valid_to_exchange_carrots(gamestate, value)
169
+ raise InvalidMoveException("Es können nicht #{value} Karotten aufgenommen werden. " + message) unless valid
170
+ gamestate.current_player.carrots += value
171
+ gamestate.set_last_action(self)
172
+ end
255
173
 
256
- required_movement = 1
257
- if gamestate.board.field(target_x, target_y).type == FieldType::LOG
258
- required_movement += 1
259
- end
260
- if required_movement > current_player.movement
261
- invalid 'Nicht genug Bewegungspunkte zum abdrängen '\
262
- "(brauche #{required_movement})"
263
- end
174
+ def type
175
+ :exchange_carrots
176
+ end
264
177
 
265
- current_player.movement -= required_movement
178
+ def ==(other)
179
+ other.type == type && other.value == value
180
+ end
181
+ end
266
182
 
267
- gamestate.other_player.x = target_x
268
- gamestate.other_player.y = target_y
183
+ class FallBack < Action
184
+ def perform!(gamestate)
185
+ valid, message = GameRules.is_valid_to_fall_back(gamestate)
186
+ raise InvalidMoveException("Es kann gerade kein Rückzug gemacht werden. " + message) unless valid
187
+ gamestate.current_player.index = gamestate.previous_field_of_type(FieldType::HEDGEHOG, gamestate.current_player.index).index
188
+ gamestate.set_last_action(self)
269
189
  end
270
190
 
271
191
  def type
272
- :push
192
+ :fall_back
273
193
  end
274
194
 
275
195
  def ==(other)
276
- other.type == type && other.direction == direction
196
+ other.type == type
277
197
  end
278
198
  end