software_challenge_client 0.1.5 → 0.2.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +6 -0
  5. data/AUTHORS +6 -0
  6. data/Guardfile +44 -0
  7. data/README.md +45 -0
  8. data/RELEASES.md +4 -0
  9. data/develop.sh +3 -0
  10. data/example/client.rb +45 -17
  11. data/example/main.rb +1 -1
  12. data/generate-authors.sh +19 -0
  13. data/lib/software_challenge_client.rb +18 -15
  14. data/lib/software_challenge_client/action.rb +278 -0
  15. data/lib/software_challenge_client/board.rb +74 -289
  16. data/lib/software_challenge_client/client_interface.rb +8 -3
  17. data/lib/software_challenge_client/condition.rb +2 -4
  18. data/lib/software_challenge_client/debug_hint.rb +3 -25
  19. data/lib/software_challenge_client/direction.rb +39 -0
  20. data/lib/software_challenge_client/field.rb +34 -12
  21. data/lib/software_challenge_client/field_type.rb +29 -8
  22. data/lib/software_challenge_client/field_unavailable_exception.rb +17 -0
  23. data/lib/software_challenge_client/game_state.rb +88 -255
  24. data/lib/software_challenge_client/invalid_move_exception.rb +16 -0
  25. data/lib/software_challenge_client/logging.rb +24 -0
  26. data/lib/software_challenge_client/move.rb +36 -35
  27. data/lib/software_challenge_client/network.rb +16 -19
  28. data/lib/software_challenge_client/player.rb +47 -10
  29. data/lib/software_challenge_client/player_color.rb +8 -7
  30. data/lib/software_challenge_client/protocol.rb +131 -83
  31. data/lib/software_challenge_client/runner.rb +9 -7
  32. data/lib/software_challenge_client/version.rb +1 -1
  33. data/release.sh +9 -0
  34. data/software_challenge_client.gemspec +20 -8
  35. metadata +175 -7
  36. data/lib/software_challenge_client/connection.rb +0 -56
@@ -0,0 +1,39 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'typesafe_enum'
4
+ # A direction on the hexagonal board of the game. Possible directions are:
5
+ # * RIGHT
6
+ # * UP_RIGHT
7
+ # * UP_LEFT
8
+ # * LEFT
9
+ # * DOWN_LEFT
10
+ # * DOWN_RIGHT
11
+ # Access them as Direction::RIGHT for example.
12
+ class Direction < TypesafeEnum::Base
13
+ new :RIGHT
14
+ new :UP_RIGHT
15
+ new :UP_LEFT
16
+ new :LEFT
17
+ new :DOWN_LEFT
18
+ new :DOWN_RIGHT
19
+
20
+ # @param direction [Direction] starting direction
21
+ # @param turns [Integer] number of turns (positive means turning
22
+ # counterclockwise).
23
+ # @return [Direction] The direction when turning the given number of steps
24
+ # from the given direction.
25
+ def self.get_turn_direction(direction, turns)
26
+ # order of directions is equal to counterclockwise turning
27
+ Direction.find_by_ord((direction.ord + turns) % 6)
28
+ end
29
+
30
+ # @return [Turn] The Turn action to get from from_direction to to_direction.
31
+ def self.from_to(from_direction, to_direction)
32
+ distance = (to_direction.ord - from_direction.ord + 6) % 6
33
+ if distance > 3
34
+ distance = distance - 6
35
+ end
36
+ Turn.new(distance)
37
+ end
38
+
39
+ end
@@ -1,13 +1,8 @@
1
1
  # encoding: UTF-8
2
- require_relative 'player_color'
3
2
  require_relative 'field_type'
4
3
 
5
- # @author Ralf-Tobias Diekert
6
- # A field on the game board
4
+ # A field on the game board.
7
5
  class Field
8
- # @!attribute [rw] ownerColor
9
- # @return [PlayerColor] the field's owner's color
10
- attr_accessor :ownerColor
11
6
  # @!attribute [rw] type
12
7
  # @return [PlayerColor] the field's type
13
8
  attr_accessor :type
@@ -18,26 +13,53 @@ class Field
18
13
  # @return [Integer] the field's y-coordinate
19
14
  attr_reader :y
20
15
 
16
+ # @!attribute [r] direction
17
+ # @return [Integer] the direction of the tile which the field belongs to
18
+ attr_reader :direction
19
+
20
+ # @!attribute [r] index
21
+ # @return [Integer] the index of the tile which the field belongs to
22
+ attr_reader :index
23
+
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
+
21
28
  # Initializer
22
29
  #
23
30
  # @param type [FieldType] field type
24
31
  # @param x [Integer] x-coordinate
25
32
  # @param y [Integer] y-coordinate
26
- def initialize(type, x, y)
27
- self.ownerColor = PlayerColor::NONE
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)
28
37
  self.type = type
29
38
  @x = x
30
39
  @y = y
40
+ @index = index
41
+ @direction = direction
42
+ @points = points
31
43
  end
32
44
 
33
45
  def ==(another_field)
34
- return self.ownerColor == another_field.ownerColor &&
35
- self.type == another_field.type &&
46
+ return self.type == another_field.type &&
36
47
  self.x == another_field.x &&
37
48
  self.y == another_field.y
38
49
  end
39
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
60
+ end
61
+
40
62
  def to_s
41
- return "Field: x = #{self.x}, y = #{self.y}, owner = #{self.ownerColor}, type = #{self.type}"
63
+ return "Field: x = #{self.x}, y = #{self.y}, type = #{self.type}"
42
64
  end
43
- end
65
+ end
@@ -1,9 +1,30 @@
1
1
  # encoding: UTF-8
2
- # @author Ralf-Tobias Diekert
3
- #Fieldtype constants
4
- module FieldType
5
- NORMAL = 1
6
- RED = 2
7
- BLUE = 4
8
- SWAMP = 8
9
- end
2
+
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.
17
+ 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
+
30
+ end
@@ -0,0 +1,17 @@
1
+ # encoding: UTF-8
2
+ # Exception indicating that a requested field does not exist.
3
+ class FieldUnavailableException < StandardError
4
+ # @!attribute [r] x
5
+ # @return [Integer] the X-coordinate of the requested field.
6
+ attr_reader :x
7
+
8
+ # @!attribute [r] y
9
+ # @return [Integer] the Y-coordinate of the requested field.
10
+ attr_reader :y
11
+
12
+ def initialize(x, y)
13
+ super("Field with coordinates (#{x},#{y}) is not available.")
14
+ @x = x
15
+ @y = y
16
+ end
17
+ end
@@ -1,25 +1,22 @@
1
- # encoding: UTF-8
1
+ # encoding: utf-8
2
2
  require_relative './util/constants'
3
3
  require_relative 'player'
4
4
  require_relative 'board'
5
5
  require_relative 'move'
6
6
  require_relative 'condition'
7
- require_relative 'player_color'
8
7
  require_relative 'field_type'
9
8
 
10
- # @author Ralf-Tobias Diekert
11
- # The state of a game
9
+ # The state of a game, as received from the server.
12
10
  class GameState
13
-
14
11
  # @!attribute [rw] turn
15
12
  # @return [Integer] turn number
16
13
  attr_accessor :turn
17
- # @!attribute [rw] startPlayerColor
14
+ # @!attribute [rw] start_player_color
18
15
  # @return [PlayerColor] the start-player's color
19
- attr_accessor :startPlayerColor
20
- # @!attribute [rw] currentPlayerColor
16
+ attr_accessor :start_player_color
17
+ # @!attribute [rw] current_player_color
21
18
  # @return [PlayerColor] the current player's color
22
- attr_accessor :currentPlayerColor
19
+ attr_accessor :current_player_color
23
20
  # @!attribute [r] red
24
21
  # @return [Player] the red player
25
22
  attr_reader :red
@@ -35,310 +32,146 @@ class GameState
35
32
  # @!attribute [rw] condition
36
33
  # @return [Condition] the winner and winning reason
37
34
  attr_accessor :condition
35
+ # @!attribute [rw] free_acceleration
36
+ # @return [Boolean] true if the free acceleration for this turn is still
37
+ # available.
38
+ attr_accessor :free_acceleration
39
+ alias free_acceleration? free_acceleration
40
+
41
+ # @!attribute [rw] free_turn
42
+ # @return [Boolean] True if the free turning for this turn is still
43
+ # available.
44
+ attr_accessor :free_turn
45
+ alias free_turn? free_turn
46
+
47
+ # @!attribute [rw] additional_free_turn_after_push
48
+ # @return [Boolean] True if the free turning for this turn is still
49
+ # available.
50
+ attr_accessor :additional_free_turn_after_push
51
+ alias additional_free_turn_after_push? additional_free_turn_after_push
52
+
53
+ POINTS_PER_TILE = 5
54
+ POINTS_PER_PASSENGER = 5
38
55
 
39
56
  def initialize
40
- self.currentPlayerColor = PlayerColor::RED
41
- self.startPlayerColor = PlayerColor::RED
42
- self.board = Board.new(true)
57
+ @current_player_color = PlayerColor::RED
58
+ @start_player_color = PlayerColor::RED
59
+ @board = Board.new
60
+ @free_acceleration = true
61
+ @free_turn = true
62
+ @additional_free_turn_after_push = false
43
63
  end
44
64
 
45
65
  # adds a player to the gamestate
46
66
  #
47
67
  # @param player [Player] the player, that will be added
48
- def addPlayer(player)
68
+ def add_player(player)
49
69
  if player.color == PlayerColor::RED
50
70
  @red = player
51
- else if player.color == PlayerColor::BLUE
52
- @blue = player
53
- end
71
+ elsif player.color == PlayerColor::BLUE
72
+ @blue = player
54
73
  end
55
74
  end
56
75
 
57
76
  # gets the current player
58
77
  #
59
78
  # @return [Player] the current player
60
- def currentPlayer
61
- if self.currentPlayerColor == PlayerColor::RED
62
- return self.red
63
- else
64
- return self.blue
79
+ def current_player
80
+ if current_player_color == PlayerColor::RED
81
+ then red
82
+ else blue
65
83
  end
66
84
  end
67
85
 
68
86
  # gets the other (not the current) player
69
87
  #
70
88
  # @return [Player] the other (not the current) player
71
- def otherPlayer
72
- if self.currentPlayerColor == PlayerColor::RED
73
- return self.blue
89
+ def other_player
90
+ if current_player_color == PlayerColor::RED
91
+ return blue
74
92
  else
75
- return self.red
93
+ return red
76
94
  end
77
95
  end
78
96
 
79
97
  # gets the other (not the current) player's color
80
98
  #
81
99
  # @return [PlayerColor] the other (not the current) player's color
82
- def otherPlayerColor
83
- return PlayerColor.opponentColor(self.currentPlayerColor)
84
- end
85
-
86
- # gets the start player
87
- #
88
- # @return [Player] the startPlayer
89
- def startPlayer
90
- if self.startPlayer == PlayerColor::RED
91
- return self.red
92
- else
93
- return self.blue
94
- end
95
- end
96
-
97
- # switches current player
98
- def switchCurrentPlayer
99
- if currentPlayer.color == PlayerColor::RED
100
- @currentPlayer = self.blue
101
- else
102
- @currentPlayer = self.red
103
- end
104
- end
105
-
106
- # prepares next turn and sets the last move
107
- #
108
- # @param [Move] the last move
109
- def prepareNextTurn(lastMove)
110
- @turn++
111
- @lastMove = lastMove;
112
- self.switchCurrentPlayer()
100
+ def other_player_color
101
+ PlayerColor.opponent_color(current_player_color)
113
102
  end
114
103
 
115
104
  # gets the current round
116
105
  #
117
106
  # @return [Integer] the current round
118
107
  def round
119
- return self.turn / 2
120
- end
121
-
122
- # gets all possible moves
123
- #
124
- # @return [Array<Move>] a list of all possible moves
125
- def getPossibleMoves
126
- enemyFieldType = currentPlayer.color == PlayerColor::RED ? FieldType::BLUE : FieldType::RED
127
- moves = Array.new
128
- for x in 0..(Constants::SIZE-1)
129
- for y in 0..(Constants::SIZE-1)
130
- if (self.board.fields[x][y].ownerColor == PlayerColor::NONE &&
131
- self.board.fields[x][y].type != FieldType::SWAMP &&
132
- self.board.fields[x][y].type != enemyFieldType)
133
- moves.push(Move.new(x, y))
134
- end
135
- end
136
- end
137
- return moves
108
+ turn / 2
138
109
  end
139
110
 
140
111
  # performs a move on the gamestate
141
112
  #
142
113
  # @param move [Move] the move, that will be performed
143
114
  # @param player [Player] the player, who makes the move
144
- def perform(move, player)
145
- if !move.nil?
146
- if move.x < Constants::SIZE && move.y < Constants::SIZE &&
147
- move.x >= 0 && move.y >= 0
148
- if self.getPossibleMoves.include?(move)
149
- self.board.put(move.x, move.y, player)
150
- player.points = self.pointsForPlayer(player)
151
- else
152
- raise "Der Zug ist nicht möglich, denn der Platz ist bereits besetzt oder nicht besetzbar."
153
- end
154
- else
155
- raise "Startkoordinaten sind nicht innerhalb des Spielfeldes."
156
- end
157
- end
158
- end
159
-
160
- # gets a player's points
161
- #
162
- # @param player [Player] the player, whos statistics will be returned
163
- # @return [Integer] the points of the player
164
- def playerStats(player)
165
- return self.playerStats(player.color)
166
- end
167
-
168
- # gets a player's points by the player's color
169
- #
170
- # @param playerColor [PlayerColor] the player's color, whos statistics will be returned
171
- # @return [Integer] the points of the player
172
- def playerStats(playerColor)
173
- if playerColor == PlayerColor::RED
174
- return self.gameStats[0];
175
- else
176
- return self.gameStats[1]
177
- end
178
- end
179
-
180
- # gets the players' statistics
181
- #
182
- # @return [Array<Integer>] the points for both players
183
- def gameStats
184
- stats = Array.new(2, Array.new(1))
185
-
186
- stats[0][0] = self.red.points
187
- stats[1][0] = self.blue.points
188
-
189
- return stats
190
- end
191
-
192
- # get the players' names
193
- #
194
- # @return [Array<String>] the names for both players
195
- def playerNames
196
- return [red.displayName, blue.displayName]
197
- end
198
-
199
- # sets the game-ended condition
200
- #
201
- # @param winner [Player] the winner of the game
202
- # @param reason [String] the winning reason
203
- def endGame(winner, reason)
204
- if condition.nil?
205
- @condition = Condition.new(winner, reason)
115
+ def perform!(move, player)
116
+ move.actions.each do |action|
117
+ action.perform!(self, player)
206
118
  end
207
119
  end
208
120
 
209
121
  # has the game ended?
210
122
  #
211
123
  # @return [Boolean] true, if the game has allready ended
212
- def gameEnded?
213
- return !self.condition.nil?
124
+ def game_ended?
125
+ !condition.nil?
214
126
  end
215
127
 
216
128
  # gets the game's winner
217
129
  #
218
130
  # @return [Player] the game's winner
219
131
  def winner
220
- return condition.nil? ? nil : self.condition.winner
132
+ condition.nil? ? nil : condition.winner
221
133
  end
222
134
 
223
135
  # gets the winning reason
224
136
  #
225
137
  # @return [String] the winning reason
226
- def winningReason
227
- return condition.nil? ? nil : self.condition.reason
138
+ def winning_reason
139
+ condition.nil? ? nil : condition.reason
228
140
  end
229
141
 
230
- # calculates a player's points
142
+ # calculates a player's points based on the current gamestate
231
143
  #
232
- # @param player [Player] the player, whos point will be calculated
144
+ # @param player [Player] the player, whos point will be calculated
233
145
  # @return [Integer] the points of the player
234
- def pointsForPlayer(player)
235
- playerColor = player.color
236
- longestPath = 0
237
-
238
- outermostFieldsInCircuit = Array.new(Array.new)
239
- visited = Array.new(Constants::SIZE).map {|j| Array.new(Constants::SIZE).map {|i| false}} #// all by default initialized to false
240
- for x in 0..(Constants::SIZE-1)
241
- for y in 0..(Constants::SIZE-1)
242
- if visited[x][y] == false
243
- if self.board.fields[x][y].ownerColor == playerColor
244
- startOfCircuit = Array.new
245
- startOfCircuit.push(self.board.fields[x][y])
246
- circuit = self.circuit(startOfCircuit, Array.new)
247
- for f in circuit
248
- visited[f.x][f.y] = true
249
- end
250
- outermost = Array.new(2)
251
- if playerColor == PlayerColor::RED
252
-
253
- outermost[0] = self.bottomMostFieldInCircuit(circuit)
254
- outermost[1] = self.topMostFieldInCircuit(circuit)
255
- else
256
- outermost[0] = leftMostFieldInCircuit(circuit)
257
- outermost[1] = rightMostFieldInCircuit(circuit)
258
- end
259
- outermostFieldsInCircuit.push(outermost)
260
-
261
- end
262
- visited[x][y] = true
263
- end
264
- end
265
- end
266
- for fields in outermostFieldsInCircuit
267
- if (playerColor == PlayerColor::RED && fields[1].y - fields[0].y > longestPath)
268
- longestPath = fields[1].y - fields[0].y
269
- end
270
- if (playerColor == PlayerColor::BLUE && fields[1].x - fields[0].x > longestPath)
271
- longestPath = fields[1].x - fields[0].x
272
- end
273
- end
274
-
275
- return longestPath # return longestPath
276
- end
277
-
278
- # the following functions are helpers for the points calculation
279
- def bottomMostFieldInCircuit(circuit)
280
- bottomMostField = circuit[0]
281
- for f in circuit
282
- if f.y < bottomMostField.y
283
- bottomMostField = f
284
- end
285
- end
286
- return bottomMostField
287
- end
288
-
289
- def topMostFieldInCircuit(circuit)
290
- topMostField = circuit[0]
291
- for f in circuit
292
- if f.y > topMostField.y
293
- topMostField = f
294
- end
295
- end
296
- return topMostField
297
- end
298
-
299
- def leftMostFieldInCircuit(circuit)
300
- leftMostField = circuit[0]
301
- for f in circuit
302
- if f.x < leftMostField.x
303
- leftMostField = f
304
- end
305
- end
306
- return leftMostField
307
- end
308
-
309
- def rightMostFieldInCircuit(circuit)
310
- rightMostField = circuit[0]
311
- for f in circuit
312
- if f.x > rightMostField.x
313
- rightMostField = f
314
- end
315
- end
316
- return rightMostField
317
- end
318
-
319
- def circuit(circuit, visited)
320
- changed = false;
321
- toBeAddedFields = Array.new
322
- for f in circuit
323
- if !visited.include?(f)
324
- changed = true
325
- visited.push(f)
326
- for c in self.board.getConnections(f.x,f.y)
327
- if !circuit.include?(self.board.fields[c.x2][c.y2])
328
- toBeAddedFields.push(self.board.fields[c.x2][c.y2])
329
- end
330
- if !circuit.include?(self.board.fields[c.x1][c.y1])
331
- toBeAddedFields.push(self.board.fields[c.x1][c.y1])
332
- end
333
- end
334
- end
335
- end
336
- circuit.push(*toBeAddedFields)
337
- if changed
338
- return self.circuit(circuit, visited)
339
- else
340
- return circuit
341
- end
342
- end
343
-
344
- end
146
+ def points_for_player(player)
147
+ player_field = board.field(player.x, player.y)
148
+ POINTS_PER_TILE * player_field.index +
149
+ POINTS_PER_PASSENGER * player.passengers +
150
+ player_field.points
151
+ end
152
+
153
+ # @return [Boolean] true if the given field is occupied by the other (not
154
+ # current) player.
155
+ def occupied_by_other_player?(field)
156
+ field.x == other_player.x && field.y == other_player.y
157
+ end
158
+
159
+ # Compared with other state.
160
+ def ==(other)
161
+ turn == other.turn &&
162
+ start_player_color == other.start_player_color &&
163
+ current_player_color == other.current_player_color &&
164
+ red == other.red &&
165
+ blue == other.blue &&
166
+ board == other.board &&
167
+ lastMove == other.lastMove &&
168
+ free_acceleration == other.free_acceleration &&
169
+ condition == other.condition
170
+ end
171
+
172
+ # Create a deep copy of the gamestate. Can be used to perform moves on without
173
+ # changing the original gamestate.
174
+ def deep_clone
175
+ Marshal.load(Marshal.dump(self))
176
+ end
177
+ end