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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3f271f5ffeec2737047b1f9a9b3a0bcac709e3ba
4
- data.tar.gz: a36d50d1de945f9b1a101a4bc6d3398d25f9eacf
3
+ metadata.gz: 0f6e73a26e7e73031e6e8753b4530a3a97d7c6d4
4
+ data.tar.gz: 94b865f5a9919c92fdd383c7ee130b5a8444cc2a
5
5
  SHA512:
6
- metadata.gz: 38ccd36b8e494e1c6c7f23ba921f2a2ff6918c0148d2343438c4c6dd064fc4bd9b22308a43ad61c733ff2b9d3a45ed545de466a6e40dcc7fe64401e2530a90fc
7
- data.tar.gz: 38c78da170187d31242a3661a79fb128d114da59ee940d75bb9e429c1c38c20e6de477627651f25b64821c16dda71002c68165c294b70597cfb92f0ae32af624
6
+ metadata.gz: 2af181e53250e162c328d70512f12e98c01535f5cea29570dd787c34a2627575d73bd8ee832e4b72ffd33e390d15b064014b0f6d6019269cccd73c4d21d50a9a
7
+ data.tar.gz: 57434f1b54bd0723e8d1af763780cc3ad25cb79ec0476eeb924d1b812d4862e05451a1b88baf98a9816860e0c4b8c9d18c0b7ffb348e129a93f917aac8d32922
data/.gitignore CHANGED
@@ -9,3 +9,4 @@
9
9
  /tmp/
10
10
  /bin/
11
11
  /vendor
12
+ /spec/examples.txt
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --color
2
2
  --require spec_helper
3
+ --format Fuubar
data/.rubocop.yml ADDED
@@ -0,0 +1,6 @@
1
+ AllCops:
2
+ DefaultFormatter: fuubar
3
+ DisplayStyleGuide: true
4
+ DisplayCopNames: false
5
+ TargetRubyVersion: 2.0
6
+ #require: rubocop-rspec
data/AUTHORS ADDED
@@ -0,0 +1,6 @@
1
+ # This file lists all individuals having contributed content to the repository.
2
+ # For how it is generated, see `generate-authors.sh`.
3
+
4
+ kwollw <kwollw@users.noreply.github.com>
5
+ Ralf-Tobias Diekert <ralfbias@hotmail.com>
6
+ Sven Koschnicke <s.koschnicke@gfxpro.com>
data/Guardfile ADDED
@@ -0,0 +1,44 @@
1
+ # encoding: UTF-8
2
+ # A sample Guardfile
3
+ # More info at https://github.com/guard/guard#readme
4
+
5
+ ## Uncomment and set this to only include directories you want to watch
6
+ directories %w(lib lib/software_challenge_client spec)
7
+ # the following seems to cause problems in certain ruby versions:
8
+ # directories %w(lib lib/software_challenge_client spec).select do |d|
9
+ # Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")
10
+ # end
11
+
12
+ ## Note: if you are using the `directories` clause above and you are not
13
+ ## watching the project directory ('.'), then you will want to move
14
+ ## the Guardfile to a watched dir and symlink it back, e.g.
15
+ #
16
+ # $ mkdir config
17
+ # $ mv Guardfile config/
18
+ # $ ln -s config/Guardfile .
19
+ #
20
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
21
+
22
+ # This group allows to skip running RuboCop when RSpec failed.
23
+ group :red_green_refactor, halt_on_fail: true do
24
+ guard :rspec, cmd: 'rspec', all_after_pass: true, all_on_start: true do
25
+ watch(%r{^lib/software_challenge_client/(.+)\.rb$}) do |match|
26
+ spec_file = "spec/#{match[1]}_spec.rb"
27
+ if File.exist?(spec_file)
28
+ spec_file # run spec belonging to the file which was changed
29
+ else
30
+ 'spec' # run all specs
31
+ end
32
+ end
33
+ watch(%r{^spec/.+_spec\.rb$}) # no block means the matched file is returned
34
+
35
+ watch('lib/software_challenge_client.rb') { 'spec' }
36
+ watch('spec/spec_helper.rb') { 'spec' }
37
+ end
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
44
+ end
data/README.md CHANGED
@@ -33,6 +33,37 @@ in a shell (while being in the example directory). Note that the
33
33
  software_challenge_client gem needs to be installed for this to work and a
34
34
  server waiting for a manual client has to be running.
35
35
 
36
+ ## Documentation
37
+
38
+ Code documentation can be generated using YARD in the project root (source code
39
+ needs to be checked out and `bundle` has to be executed,
40
+ see [Installation](#installation)):
41
+
42
+ ```console
43
+ yard
44
+ ```
45
+
46
+ After generation, the docs can be found in the `doc` directory. Start at
47
+ `index.html`.
48
+
49
+ Documentation for the latest source can also be found
50
+ on
51
+ [rubydoc.info](http://www.rubydoc.info/github/CAU-Kiel-Tech-Inf/socha_ruby_client).
52
+
53
+ When updating the docs, you may use
54
+
55
+ ```console
56
+ yard server --reload
57
+ ```
58
+
59
+ or inside a docker container
60
+
61
+ ```console
62
+ yard server --reload --bind 0.0.0.0
63
+ ```
64
+
65
+ to get a live preview of them at [http://localhost:8808](http://localhost:8808).
66
+
36
67
  ## Development
37
68
 
38
69
  After checking out the repo, run `bin/setup` to install
@@ -43,6 +74,20 @@ experiment.
43
74
  To install this gem onto your local machine, run `bundle exec rake
44
75
  install`.
45
76
 
77
+ To develop inside a docker container, use the included `Dockerfile` and
78
+ `develop.sh`.
79
+
80
+ ### Specs
81
+
82
+ The gem is tested using RSpec. To run all tests, execute `rspec`. When
83
+ developing, you may use Guard to execute tests when files change. To do this,
84
+ execute `guard`. Tests will then be automatically run when you change a file.
85
+
86
+ ### Linting
87
+
88
+ Linting by rubocop is included in the guard config. It is run when all specs
89
+ pass.
90
+
46
91
  ### Releasing
47
92
 
48
93
  To release a new version, update the version number in
data/RELEASES.md CHANGED
@@ -1,3 +1,7 @@
1
+ = 0.2.0
2
+
3
+ - First working version for Mississippi Queen
4
+
1
5
  = 0.1.5
2
6
 
3
7
  - Fixed bug in reservation code.
data/develop.sh ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ set -x # echo commands as they are executed
3
+ docker run -it --rm -p 8808:8808 -v "$PWD":/usr/src/app -w /usr/src/app ruby:latest /bin/bash
data/example/client.rb CHANGED
@@ -1,31 +1,59 @@
1
1
  # encoding: UTF-8
2
2
  require 'software_challenge_client'
3
3
 
4
+ # This is an example of a client playing the game using the software challenge
5
+ # gem.
4
6
  class Client < ClientInterface
7
+ include Logging
8
+
5
9
  attr_accessor :gamestate
6
10
 
7
- def initialize
8
- puts "Zufallsspieler erstellt."
11
+ def initialize(log_level)
12
+ logger.level = log_level
13
+ logger.info 'Einfacher Spieler wurde erstellt.'
9
14
  end
10
15
 
11
16
  # gets called, when it's your turn
12
- def getMove
13
- puts "Spielstand: #{self.gamestate.pointsForPlayer(self.gamestate.currentPlayer)} - #{self.gamestate.pointsForPlayer(self.gamestate.otherPlayer)}"
14
- mov = self.randomMove
15
- unless mov.nil?
16
- puts 'Zug gefunden: '
17
- puts mov.to_s
18
- end
19
- return mov
17
+ def move_requested
18
+ 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
20
22
  end
21
23
 
22
- # choose a random move
23
- def randomMove
24
- possibleMoves = self.gamestate.getPossibleMoves
25
- if possibleMoves.length > 0
26
- return possibleMoves[SecureRandom.random_number(possibleMoves.length)]
27
- else
28
- return nil
24
+ # choose a move giving the most points
25
+ 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
51
+ end
52
+ rescue InvalidMoveException => e
53
+ logger.debug("Zug #{move} ist ungueltig: #{e}")
54
+ end
55
+ end
29
56
  end
57
+ best
30
58
  end
31
59
  end
data/example/main.rb CHANGED
@@ -37,6 +37,6 @@ end
37
37
 
38
38
  opt_parser.parse!(ARGV)
39
39
 
40
- client = Client.new
40
+ client = Client.new(Logger::DEBUG)
41
41
  runner = Runner.new(options.host, options.port, client, options.reservation)
42
42
  runner.start()
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+ # Thanks to the docker project!
3
+ # https://github.com/docker/docker/blob/2c224e4fc09518d33780d818cf74026f6aa32744/hack/generate-authors.sh
4
+ set -e
5
+
6
+ # change to the directory where the script is located, this should be the project root
7
+ pushd "$(dirname "$(readlink -f "$BASH_SOURCE")")/"
8
+
9
+ # see also ".mailmap" for how email addresses and names are deduplicated
10
+
11
+ {
12
+ cat <<-'EOH'
13
+ # This file lists all individuals having contributed content to the repository.
14
+ # For how it is generated, see `generate-authors.sh`.
15
+ EOH
16
+ echo
17
+ git log --format='%aN <%aE>' | LC_ALL=C.UTF-8 sort -uf
18
+ } > AUTHORS
19
+ popd
@@ -1,18 +1,21 @@
1
1
  # encoding: UTF-8
2
2
  module SoftwareChallengeClient
3
- require "software_challenge_client/version"
4
- require "software_challenge_client/board"
5
- require "software_challenge_client/client_interface"
6
- require "software_challenge_client/condition"
7
- require "software_challenge_client/connection"
8
- require "software_challenge_client/debug_hint"
9
- require "software_challenge_client/field"
10
- require "software_challenge_client/field_type"
11
- require "software_challenge_client/game_state"
12
- require "software_challenge_client/move"
13
- require "software_challenge_client/network"
14
- require "software_challenge_client/player"
15
- require "software_challenge_client/player_color"
16
- require "software_challenge_client/protocol"
17
- require "software_challenge_client/runner"
3
+ require 'software_challenge_client/version'
4
+ require 'software_challenge_client/logging'
5
+ require 'software_challenge_client/invalid_move_exception'
6
+ require 'software_challenge_client/field_unavailable_exception'
7
+ require 'software_challenge_client/board'
8
+ require 'software_challenge_client/client_interface'
9
+ require 'software_challenge_client/condition'
10
+ require 'software_challenge_client/debug_hint'
11
+ require 'software_challenge_client/field'
12
+ require 'software_challenge_client/field_type'
13
+ require 'software_challenge_client/game_state'
14
+ require 'software_challenge_client/move'
15
+ require 'software_challenge_client/network'
16
+ require 'software_challenge_client/player'
17
+ require 'software_challenge_client/player_color'
18
+ require 'software_challenge_client/protocol'
19
+ require 'software_challenge_client/runner'
20
+ require 'software_challenge_client/direction'
18
21
  end
@@ -0,0 +1,278 @@
1
+ # encoding: utf-8
2
+
3
+ # An action is a part of a move. A move can have multiple actions. The specific
4
+ # actions are inherited from this Action class which should be considered
5
+ # abstract/interface.
6
+ class Action
7
+ # @return [ActionType] Type of the action.
8
+ def type
9
+ raise 'must be overridden'
10
+ end
11
+
12
+ def ==(_other)
13
+ raise 'must be overridden'
14
+ end
15
+
16
+ def perform!(_gamestate, _current_player)
17
+ raise 'must be overridden'
18
+ end
19
+
20
+ # Helper to make raising InvalidMoveExceptions easier. It is defined in the
21
+ # Action class instead of the Move class because performing individual actions
22
+ # normally trigger invalid moves, not the move itself.
23
+ #
24
+ # @param message [String] Message why the move is invalid.
25
+ # @return Nothing. Raises an exception.
26
+ def invalid(message)
27
+ raise InvalidMoveException.new(message, self)
28
+ end
29
+ end
30
+
31
+ # Accelerate by {#acceleration}. To decelerate, use a negative value.
32
+ class Acceleration < Action
33
+ attr_reader :acceleration
34
+
35
+ def initialize(acceleration)
36
+ @acceleration = acceleration
37
+ end
38
+
39
+ # Perform the action.
40
+ #
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.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
59
+ 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
+ end
68
+
69
+ def type
70
+ :acceleration
71
+ end
72
+
73
+ def ==(other)
74
+ other.type == type && other.acceleration == acceleration
75
+ end
76
+ end
77
+
78
+ # Turn by {#direction}.
79
+ class Turn < Action
80
+ # Number of steps to turn. Negative values for turning clockwise, positive for
81
+ # counterclockwise.
82
+ attr_reader :direction
83
+
84
+ def initialize(direction)
85
+ @direction = direction
86
+ end
87
+
88
+ # (see Acceleration#perform!)
89
+ def perform!(gamestate, current_player)
90
+ invalid 'Drehung um 0 ist ungültig' if direction.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 = direction.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 #{direction}. "\
105
+ "Habe #{current_player.coal}, brauche #{needed_coal}."
106
+ end
107
+
108
+ current_player.direction =
109
+ Direction.get_turn_direction(current_player.direction, direction)
110
+ current_player.coal -= [0, needed_coal].max
111
+ gamestate.free_turn = false
112
+ end
113
+
114
+ def type
115
+ :turn
116
+ end
117
+
118
+ def ==(other)
119
+ other.type == type && other.direction == direction
120
+ end
121
+ end
122
+
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
130
+ end
131
+
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
198
+
199
+ end
200
+
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(:+)
213
+ end
214
+
215
+ def type
216
+ :advance
217
+ end
218
+
219
+ def ==(other)
220
+ other.type == type && other.distance == distance
221
+ end
222
+ end
223
+
224
+ # Push the opponent in {#direction}
225
+ class Push < Action
226
+ # @return [Direction] the direction where to push.
227
+ attr_reader :direction
228
+
229
+ # @param direction [Direction]
230
+ def initialize(direction)
231
+ @direction = direction
232
+ end
233
+
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
+ )
255
+
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
264
+
265
+ current_player.movement -= required_movement
266
+
267
+ gamestate.other_player.x = target_x
268
+ gamestate.other_player.y = target_y
269
+ end
270
+
271
+ def type
272
+ :push
273
+ end
274
+
275
+ def ==(other)
276
+ other.type == type && other.direction == direction
277
+ end
278
+ end