software_challenge_client 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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