software_challenge_client 20.2.4 → 21.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/.ruby-version +1 -1
  4. data/.vscode/launch.json +41 -0
  5. data/.vscode/settings.json +10 -0
  6. data/Dockerfile +1 -1
  7. data/Gemfile +1 -0
  8. data/Guardfile +1 -0
  9. data/README.md +4 -3
  10. data/RELEASES.md +20 -0
  11. data/Rakefile +4 -3
  12. data/example/client.rb +6 -9
  13. data/example/main.rb +9 -9
  14. data/lib/software_challenge_client.rb +26 -23
  15. data/lib/software_challenge_client/board.rb +94 -37
  16. data/lib/software_challenge_client/client_interface.rb +1 -0
  17. data/lib/software_challenge_client/color.rb +16 -0
  18. data/lib/software_challenge_client/condition.rb +2 -0
  19. data/lib/software_challenge_client/coordinate_set.rb +92 -0
  20. data/lib/software_challenge_client/coordinates.rb +45 -0
  21. data/lib/software_challenge_client/debug_hint.rb +1 -0
  22. data/lib/software_challenge_client/field.rb +21 -56
  23. data/lib/software_challenge_client/game_rule_logic.rb +258 -335
  24. data/lib/software_challenge_client/game_state.rb +106 -68
  25. data/lib/software_challenge_client/has_hints.rb +1 -1
  26. data/lib/software_challenge_client/invalid_move_exception.rb +1 -0
  27. data/lib/software_challenge_client/logging.rb +1 -0
  28. data/lib/software_challenge_client/network.rb +1 -1
  29. data/lib/software_challenge_client/piece.rb +71 -13
  30. data/lib/software_challenge_client/piece_shape.rb +109 -0
  31. data/lib/software_challenge_client/player.rb +7 -6
  32. data/lib/software_challenge_client/player_type.rb +14 -0
  33. data/lib/software_challenge_client/protocol.rb +81 -74
  34. data/lib/software_challenge_client/rotation.rb +22 -0
  35. data/lib/software_challenge_client/runner.rb +2 -1
  36. data/lib/software_challenge_client/set_move.rb +13 -4
  37. data/lib/software_challenge_client/skip_move.rb +5 -0
  38. data/lib/software_challenge_client/util/constants.rb +3 -4
  39. data/lib/software_challenge_client/version.rb +2 -1
  40. data/lib/update_client_module.sh +15 -0
  41. data/software_challenge_client.gemspec +15 -13
  42. metadata +54 -36
  43. data/lib/software_challenge_client/cube_coordinates.rb +0 -25
  44. data/lib/software_challenge_client/direction.rb +0 -55
  45. data/lib/software_challenge_client/drag_move.rb +0 -22
  46. data/lib/software_challenge_client/line_direction.rb +0 -15
  47. data/lib/software_challenge_client/piece_type.rb +0 -18
  48. data/lib/software_challenge_client/player_color.rb +0 -25
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  # Das Interface sollte von einem Client implementiert werden, damit er über das
4
5
  # Gem an einem Spiel teilnehmen kann.
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'typesafe_enum'
4
+
5
+ # Die Spielsteinfarben. BLUE, YELLOW, RED und GREEN
6
+ class Color < TypesafeEnum::Base
7
+ new :BLUE, 'B'
8
+ new :YELLOW, 'Y'
9
+ new :RED, 'R'
10
+ new :GREEN, 'G'
11
+
12
+ # Gibt den color namen zurück
13
+ def to_s
14
+ self.key.to_s
15
+ end
16
+ end
@@ -1,4 +1,5 @@
1
1
  # encoding: UTF-8
2
+ # frozen_string_literal: true
2
3
  require_relative 'player'
3
4
 
4
5
  # Das Ergebnis eines Spieles. Ist im `GameState#condition` zu finden, wenn das Spiel beendet wurde.
@@ -19,6 +20,7 @@ class Condition
19
20
  @reason = reason
20
21
  end
21
22
 
23
+ # Überprüfe ob es ein Unentschieden gab
22
24
  def draw?
23
25
  @winner.nil?
24
26
  end
@@ -0,0 +1,92 @@
1
+ require_relative 'util/constants'
2
+
3
+ # Eine Menge aus Koordinaten
4
+ class CoordinateSet
5
+ include Constants
6
+
7
+ # @!attribute [r] coordinates
8
+ # @return [Array<Coordinates>] Die enthaltenen Koordinaten.
9
+ attr_reader :coordinates
10
+
11
+ # Erstellt eine neue leere Koordinaten-Menge.
12
+ def initialize(coordinates)
13
+ @coordinates = coordinates
14
+ end
15
+
16
+ # Invertiert die X-Koordinate aller Koordinaten in dieser Menge
17
+ def flip(should_flip = true)
18
+ return self unless should_flip
19
+
20
+ transform do |it|
21
+ Coordinates.new(-it.x, it.y)
22
+ end.align
23
+ end
24
+
25
+ # Enumeriert die enthaltenen Koordinaten
26
+ def transform
27
+ CoordinateSet.new(
28
+ coordinates.map do |it|
29
+ yield it
30
+ end
31
+ )
32
+ end
33
+
34
+ # Gibt die Größe des kleinsten Bereichs zurück, in dem alle enthaltenen Punkte liegen
35
+ def area
36
+ minX = coordinates.map(&:x).min
37
+ minY = coordinates.map(&:y).min
38
+ maxX = coordinates.map(&:x).max
39
+ maxY = coordinates.map(&:y).max
40
+ Coordinates.new(maxX - minX + 1, maxY - minY + 1)
41
+ end
42
+
43
+ # Bewege den Bereich der enthaltenen Koordinaten zum Ursprung
44
+ def align
45
+ minX = coordinates.map(&:x).min
46
+ minY = coordinates.map(&:y).min
47
+ transform do |it|
48
+ Coordinates.new(it.x - minX, it.y - minY)
49
+ end
50
+ end
51
+
52
+ # Wende eine Rotation auf den Stein an
53
+ # @param rotation [Rotation] Die anzuwendene Rotation
54
+ # @return [CoordinateSet] Die gedrehten Koordinaten
55
+ def rotate(rotation)
56
+ case rotation
57
+ when Rotation::NONE
58
+ self
59
+ when Rotation::RIGHT
60
+ turn_right.align
61
+ when Rotation::MIRROR
62
+ mirror.align
63
+ when Rotation::LEFT
64
+ turn_left.align
65
+ end
66
+ end
67
+
68
+ # Drehe alle enthaltenen Koordinaten um 90° nach rechts
69
+ def turn_right
70
+ transform do |it|
71
+ Coordinates.new(-it.y, it.x)
72
+ end
73
+ end
74
+
75
+ # Drehe alle enthaltenen Koordinaten um 90° nach links
76
+ def turn_left
77
+ transform do |it|
78
+ Coordinates.new(it.y, -it.x)
79
+ end
80
+ end
81
+
82
+ # Spiegle alle enthaltenen Koordinaten um beide Achsen
83
+ def mirror
84
+ transform do |it|
85
+ Coordinates.new(-it.x, -it.y)
86
+ end
87
+ end
88
+
89
+ def ==(other)
90
+ coordinates.sort == other.coordinates.sort
91
+ end
92
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Einfache kartesische Koordinaten
4
+ class Coordinates
5
+ include Comparable
6
+ attr_reader :x, :y
7
+
8
+ # Erstellt neue leere Koordinaten.
9
+ def initialize(x, y)
10
+ @x = x
11
+ @y = y
12
+ end
13
+
14
+ def ==(other)
15
+ x == other.x && y == other.y
16
+ end
17
+
18
+ # Gibt die Ursprungs-Koordinaten (0, 0) zurück.
19
+ def self.origin
20
+ Coordinates.new(0, 0)
21
+ end
22
+
23
+ def <=>(other)
24
+ xComp = x <=> other.x
25
+ yComp = y <=> other.y
26
+ if xComp == 0
27
+ yComp
28
+ else
29
+ xComp
30
+ end
31
+ end
32
+
33
+ def +(other)
34
+ Coordinates.new(x + other.x, y + other.y)
35
+ end
36
+
37
+ # Gibt eine textuelle Repräsentation der Koordinaten aus.
38
+ def to_s
39
+ "(#{x}, #{y})"
40
+ end
41
+
42
+ def inspect
43
+ to_s
44
+ end
45
+ end
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  # Ein Hinweis, der zu einem Zug hinzugefügt werden kann. Z.B. zu
4
5
  # Diagnosezwecken. Der Hinweis wird in der grafischen Oberfläche angezeigt und
@@ -1,35 +1,33 @@
1
- # encoding: UTF-8
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
- # Ein Feld des Spielfelds. Ein Spielfeld ist durch die Koordinaten eindeutig identifiziert.
4
+ # Ein Feld des Spielfelds. Ein Spielfeld ist durch die Koordinaten eindeutig
5
+ # identifiziert.
4
6
  class Field
5
- # @!attribute [rw] pieces
6
- # @return [Array<Piece>] Spielsteine auf dem Feld, beginnend beim untersten Stein
7
- attr_accessor :pieces
7
+ # @!attribute [rw] color
8
+ # @return [Color] Farbe des überdeckenden Spielsteins, falls vorhanden, sonst
9
+ # nil
10
+ attr_accessor :color
8
11
  # @!attribute [r] coordinates
9
- # @return [CubeCoordinates] die Cube-Coordinates des Feldes
12
+ # @return [Coordinates] die X-Y-Koordinaten des Feldes
10
13
  attr_reader :coordinates
11
- # @!attribute [r] obstructed
12
- # @return [Boolean] ob das Feld durch eine Brombeere blockiert ist
13
- attr_reader :obstructed
14
14
 
15
- # Konstruktor
15
+ # Erstellt ein neues leeres Feld.
16
16
  #
17
17
  # @param x [Integer] X-Koordinate
18
18
  # @param y [Integer] Y-Koordinate
19
- # @param pieces [Array<Piece>] Spielsteine auf dem Feld
20
- # @param obstructed [Boolean] Ob das Feld blockiert ist (Brombeere)
21
- def initialize(x, y, pieces = [], obstructed = false)
22
- @pieces = pieces
23
- @coordinates = CubeCoordinates.new(x, y)
24
- @obstructed = obstructed
19
+ # @param color [Color] Farbe des Spielsteins, der das Feld überdeckt, nil falls kein Spielstein es überdeckt
20
+ def initialize(x, y, color = nil)
21
+ @color = color
22
+ @coordinates = Coordinates.new(x, y)
25
23
  end
26
24
 
27
- # Vergleicht zwei Felder. Felder sind gleich, wenn sie gleiche Koordinaten und gleichen Typ haben.
28
- # @return [Boolean] true bei Gleichheit, false sonst.
25
+ # Vergleicht zwei Felder. Felder sind gleich, wenn sie gleiche Koordinaten und
26
+ # gleichen Typ haben.
27
+ # @return [Boolean] true bei Gleichheit, sonst false.
29
28
  def ==(other)
30
29
  coordinates == other.coordinates &&
31
- obstructed == other.obstructed &&
32
- pieces == other.pieces
30
+ color == other.color
33
31
  end
34
32
 
35
33
  def x
@@ -40,46 +38,13 @@ class Field
40
38
  coordinates.y
41
39
  end
42
40
 
43
- def z
44
- coordinates.z
45
- end
46
-
47
- # @return [Boolean] true, wenn eine Spielsteine auf dem Feld liegen und es nicht durch eine Brombeere blockiert ist
41
+ # @return [Boolean] true, wenn das Feld nicht durch einen Spielstein überdeckt ist, sonst false
48
42
  def empty?
49
- pieces.empty? && !obstructed
50
- end
51
-
52
- # @return [Boolean] true, es nicht durch eine Brombeere blockiert ist
53
- def obstructed?
54
- obstructed
55
- end
56
-
57
- def add_piece(piece)
58
- pieces.push(piece)
59
- end
60
-
61
- # Entfernt den obersten Spielstein
62
- # @return [Piece] entfernten Spielstein oder nil
63
- def remove_piece
64
- pieces.pop
65
- end
66
-
67
- # @return [PlayerColor] Farbe des Spielers, der den obersten Spielstein kontrolliert. Ohne Spielsteine nil
68
- def color
69
- pieces.last&.color
70
- end
71
-
72
- def has_owner
73
- !color.nil?
43
+ color.nil?
74
44
  end
75
45
 
76
46
  # @return [String] Textuelle Darstellung des Feldes.
77
47
  def to_s
78
- s = "Feld #{coordinates}, "
79
- if obstructed?
80
- s += 'blockiert'
81
- else
82
- s += "Steine: #{pieces.map(&:to_s).join(', ')}"
83
- end
48
+ empty? ? '_' : color.value
84
49
  end
85
50
  end
@@ -1,411 +1,334 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require_relative './util/constants'
5
4
  require_relative 'invalid_move_exception'
5
+ require_relative 'set_move'
6
6
 
7
- # Methoden, welche die Spielregeln von Hive abbilden.
7
+ require 'set'
8
+
9
+ # Methoden, welche die Spielregeln von Blokus abbilden.
8
10
  #
9
- # Es gibt hier viele Helfermethoden, die von den beiden Hauptmethoden {GameRuleLogic#valid_move?} und {GameRuleLogic.possible_moves} benutzt werden.
11
+ # Es gibt hier viele Helfermethoden, die von den beiden Hauptmethoden {GameRuleLogic#valid_move?}
12
+ # und {GameRuleLogic.possible_moves} benutzt werden.
10
13
  class GameRuleLogic
11
-
12
14
  include Constants
13
15
 
14
- # Fügt einem leeren Spielfeld drei Brombeeren hinzu.
15
- #
16
- # Diese Methode ist dazu gedacht, ein initiales Spielbrett regelkonform zu generieren.
16
+ SUM_MAX_SQUARES = 89
17
+
18
+ # --- Possible Moves ------------------------------------------------------------
19
+
20
+ # Gibt alle möglichen Züge für den Spieler zurück, der in der gamestate dran ist.
21
+ # Diese ist die wichtigste Methode dieser Klasse für Schüler.
17
22
  #
18
- # @param board [Board] Das zu modifizierende Spielbrett. Es wird nicht
19
- # geprüft, ob sich auf dem Spielbrett bereits Brombeeren befinden.
20
- # @return [Board] Das modifizierte Spielbrett.
21
- def self.add_blocked_fields(board)
22
- raise "todo"
23
- board
24
- end
23
+ # @param gamestate [GameState] Der zu untersuchende Spielstand.
24
+ def self.possible_moves(gamestate)
25
+ re = possible_setmoves(gamestate)
25
26
 
26
- def self.get_neighbour_in_direction(board, coords, direction)
27
- board.field_at(direction.translate(coords))
28
- end
27
+ re << SkipMove.new unless gamestate.is_first_move?
29
28
 
30
- def self.get_neighbours(board, coordinates)
31
- Direction.map { |d| get_neighbour_in_direction(board, coordinates, d) }.compact
29
+ re
32
30
  end
33
31
 
34
- def self.is_bee_blocked(board, color)
35
- bee_fields = board.field_list.select { |f| f.pieces.include?(Piece.new(color, PieceType::BEE)) }
36
- return false if bee_fields.empty?
37
- return get_neighbours(board, bee_fields[0].coordinates).all? { |f| !f.empty? }
32
+ # Gibt einen zufälligen möglichen Zug zurück
33
+ # @param gamestate [GameState] Der zu untersuchende Spielstand.
34
+ def self.possible_move(gamestate)
35
+ possible_moves(gamestate).sample
38
36
  end
39
37
 
40
- # Prueft, ob ein Spielzug fuer den gegebenen Gamestate valide ist
41
- #
42
- # @param gamestate [Gamestate]
43
- # @param move [Move]
44
- # @return [?]
45
- def self.valid_move?(gamestate, move)
46
- case move
47
- when SetMove
48
- validate_set_move(gamestate, move)
49
- when DragMove
50
- validate_drag_move(gamestate, move)
51
- when SkipMove
52
- validate_skip_move(gamestate, move)
38
+ # Gibt alle möglichen Legezüge zurück
39
+ # @param gamestate [GameState] Der zu untersuchende Spielstand.
40
+ def self.possible_setmoves(gamestate)
41
+ if gamestate.is_first_move?
42
+ possible_start_moves(gamestate)
43
+ else
44
+ all_possible_setmoves(gamestate).flatten
53
45
  end
54
46
  end
55
47
 
56
- def self.is_on_board(coords)
57
- shift = (BOARD_SIZE - 1) / 2
58
- -shift <= coords.x && coords.x <= shift && -shift <= coords.y && coords.y <= shift
59
- end
48
+ # Gibt alle möglichen Legezüge in der ersten Runde zurück
49
+ # @param gamestate [GameState] Der zu untersuchende Spielstand.
50
+ def self.possible_start_moves(gamestate)
51
+ color = gamestate.current_color
52
+ shape = gamestate.start_piece
53
+ area1 = shape.dimension
54
+ area2 = Coordinates.new(area1.y, area1.x)
55
+ moves = Set[]
60
56
 
61
- def self.has_player_placed_bee(gamestate)
62
- gamestate.deployed_pieces(gamestate.current_player_color).any? { |p| p.type == PieceType::BEE }
63
- end
57
+ # Hard code corners for most efficiency (and because a proper algorithm would be pretty illegible here)
58
+ # Upper Left
59
+ moves.merge(moves_for_shape_on(color, shape, Coordinates.new(0, 0)))
64
60
 
65
- def self.validate_set_move(gamestate, move)
66
- unless is_on_board(move.destination)
67
- raise InvalidMoveException.new("Piece has to be placed on board. Destination ${move.destination} is out of bounds.", move)
68
- end
69
- unless gamestate.board.field_at(move.destination).empty?
70
- raise InvalidMoveException.new("Set destination is not empty!", move)
71
- end
61
+ # Upper Right
62
+ moves.merge(moves_for_shape_on(color, shape, Coordinates.new(BOARD_SIZE - area1.x, 0)))
63
+ moves.merge(moves_for_shape_on(color, shape, Coordinates.new(BOARD_SIZE - area2.x, 0)))
72
64
 
73
- owned_fields = gamestate.board.fields_of_color(gamestate.current_player_color)
74
- if owned_fields.empty?
75
- other_player_fields = gamestate.board.fields_of_color(gamestate.other_player_color)
76
- if !other_player_fields.empty?
77
- unless other_player_fields.map{ |of| get_neighbours(gamestate.board, of.coordinates).map{ |n| n.coordinates } }.flatten.include?(move.destination)
78
- raise InvalidMoveException.new("Piece has to be placed next to other players piece", move)
79
- end
80
- end
81
- else
82
- if gamestate.round == 3 && !has_player_placed_bee(gamestate) && move.piece.type != PieceType::BEE
83
- raise InvalidMoveException.new("The bee must be placed in fourth round latest", move)
84
- end
65
+ # Lower Left
66
+ moves.merge(moves_for_shape_on(color, shape, Coordinates.new(0, BOARD_SIZE - area1.y)))
67
+ moves.merge(moves_for_shape_on(color, shape, Coordinates.new(0, BOARD_SIZE - area2.y)))
85
68
 
86
- if !gamestate.undeployed_pieces(gamestate.current_player_color).include?(move.piece)
87
- raise InvalidMoveException.new("Piece is not a undeployed piece of the current player", move)
88
- end
69
+ # Lower Right
70
+ moves.merge(moves_for_shape_on(color, shape, Coordinates.new(BOARD_SIZE - area1.x, BOARD_SIZE - area1.y)))
71
+ moves.merge(moves_for_shape_on(color, shape, Coordinates.new(BOARD_SIZE - area2.x, BOARD_SIZE - area2.y)))
89
72
 
90
- destination_neighbours = get_neighbours(gamestate.board, move.destination)
91
- if !destination_neighbours.any? { |f| f.color == gamestate.current_player_color }
92
- raise InvalidMoveException.new("A newly placed piece must touch an own piece", move)
93
- end
94
- if destination_neighbours.any? { |f| f.color == gamestate.other_player_color }
95
- raise InvalidMoveException.new("A newly placed is not allowed to touch an opponent's piece", move)
96
- end
97
- end
98
- true
73
+ moves.select { |m| valid_set_move?(gamestate, m) }.to_a
99
74
  end
100
75
 
101
- def self.validate_skip_move(gamestate, move)
102
- if !possible_moves(gamestate).empty?
103
- raise InvalidMoveException.new("Skipping a turn is only allowed when no other moves can be made.", move)
104
- end
105
- if gamestate.round == 3 && !has_player_placed_bee(gamestate)
106
- raise InvalidMoveException.new("The bee must be placed in fourth round latest", move)
76
+ # Hilfsmethode um Legezüge für eine [PieceShape] zu berechnen.
77
+ # @param color [Color] Die Farbe der Spielsteine der Züge
78
+ # @param shape [PieceShape] Die Form der Spielsteine der Züge
79
+ # @param position [Coordinates] Die Position der Spielsteine der Züge
80
+ def self.moves_for_shape_on(color, shape, position)
81
+ moves = Set[]
82
+ Rotation.each do |r|
83
+ [true, false].each do |f|
84
+ moves << SetMove.new(Piece.new(color, shape, r, f, position))
85
+ end
107
86
  end
108
- true
87
+ moves
109
88
  end
110
89
 
111
- def self.validate_drag_move(gamestate, move)
112
- unless has_player_placed_bee(gamestate)
113
- raise InvalidMoveException.new("You have to place the bee to be able to perform dragmoves", move)
114
- end
115
-
116
- if (!is_on_board(move.destination) || !is_on_board(move.start))
117
- raise InvalidMoveException.new("The Move is out of bounds", move)
118
- end
119
-
120
- if (gamestate.board.field_at(move.start).pieces.empty?)
121
- raise InvalidMoveException.new("There is no piece to move", move)
122
- end
123
-
124
- piece_to_drag = gamestate.board.field_at(move.start).pieces.last
125
-
126
- if (piece_to_drag.owner != gamestate.current_player_color)
127
- raise InvalidMoveException.new("Trying to move piece of the other player", move)
128
- end
129
-
130
- if (move.start == move.destination)
131
- raise InvalidMoveException.new("Destination and start are equal", move)
90
+ # Gib eine Liste aller möglichen Legezüge zurück, auch wenn es die erste Runde ist.
91
+ # @param gamestate [GameState] Der zu untersuchende Spielstand.
92
+ def self.all_possible_setmoves(gamestate)
93
+ moves = []
94
+ fields = valid_fields(gamestate)
95
+ gamestate.undeployed_pieces(gamestate.current_color).each do |p|
96
+ (moves << possible_moves_for_shape(gamestate, p, fields)).flatten
132
97
  end
98
+ moves
99
+ end
133
100
 
134
- if (!gamestate.board.field_at(move.destination).pieces.empty? && piece_to_drag.type != PieceType::BEETLE)
135
- raise InvalidMoveException.new("Only beetles are allowed to climb on other Pieces", move)
101
+ # Gibt eine Liste aller möglichen SetMoves für diese Form zurück.
102
+ # @param gamestate [GameState] Der zu untersuchende Spielstand.
103
+ # @param shape Die [PieceShape], die die Züge nutzen sollen
104
+ #
105
+ # @return Alle möglichen Züge mit der Form
106
+ def self.possible_moves_for_shape(gamestate, shape, fields = valid_fields(gamestate))
107
+ color = gamestate.current_color
108
+
109
+ moves = Set[]
110
+ fields.each do |field|
111
+ shape.unique_transforms().each do |t|
112
+ piece = Piece.new(color, shape, t.r, t.f, Coordinates.new(0, 0))
113
+ piece.coords.each do |pos|
114
+ moves << SetMove.new(Piece.new(color, shape, t.r, t.f, Coordinates.new(field.x - pos.x, field.y - pos.y)))
115
+ end
116
+ end
136
117
  end
137
-
138
- board_without_piece = gamestate.board.clone
139
- board_without_piece.field_at(move.start).pieces.pop
140
-
141
- if (!is_swarm_connected(board_without_piece))
142
- raise InvalidMoveException.new("Moving piece would disconnect swarm", move)
118
+ moves.select { |m| valid_set_move?(gamestate, m) }.to_a
119
+ end
120
+
121
+ # Gibt eine Liste aller Felder zurück, an denen möglicherweise Züge gemacht werden kann.
122
+ # @param gamestate [GameState] Der zu untersuchende Spielstand.
123
+ def self.valid_fields(gamestate)
124
+ color = gamestate.current_color
125
+ board = gamestate.board
126
+ fields = Set[]
127
+ board.fields_of_color(color).each do |field|
128
+ [Coordinates.new(field.x - 1, field.y - 1),
129
+ Coordinates.new(field.x - 1, field.y + 1),
130
+ Coordinates.new(field.x + 1, field.y - 1),
131
+ Coordinates.new(field.x + 1, field.y + 1)].each do |corner|
132
+ next unless Board.contains(corner)
133
+ next unless board[corner].empty?
134
+ next if neighbor_of_color?(board, Field.new(corner.x, corner.y), color)
135
+
136
+ fields << corner
137
+ end
143
138
  end
144
-
145
- case piece_to_drag.type
146
- when PieceType::ANT
147
- validate_ant_move(board_without_piece, move)
148
- when PieceType::BEE
149
- validate_bee_move(board_without_piece, move)
150
- when PieceType::BEETLE
151
- validate_beetle_move(board_without_piece, move)
152
- when PieceType::GRASSHOPPER
153
- validate_grasshopper_move(board_without_piece, move)
154
- when PieceType::SPIDER
155
- validate_spider_move(board_without_piece, move)
139
+ fields
140
+ end
141
+
142
+ # Überprüft, ob das gegebene Feld ein Nachbarfeld mit der Farbe [color] hat
143
+ # @param board [Board] Das aktuelle Board
144
+ # @param field [Field] Das zu überprüfende Feld
145
+ # @param color [Color] Nach der zu suchenden Farbe
146
+ def self.neighbor_of_color?(board, field, color)
147
+ [Coordinates.new(field.x - 1, field.y),
148
+ Coordinates.new(field.x, field.y - 1),
149
+ Coordinates.new(field.x + 1, field.y),
150
+ Coordinates.new(field.x, field.y + 1)].any? do |neighbor|
151
+ Board.contains(neighbor) && board[neighbor].color == color
152
+ end
153
+ end
154
+
155
+ # # Return a list of all moves, impossible or not.
156
+ # # There's no real usage, except maybe for cases where no Move validation happens
157
+ # # if `Constants.VALIDATE_MOVE` is false, then this function should return the same
158
+ # # Set as `::getPossibleMoves`
159
+ # def self.get_all_set_moves()
160
+ # moves = []
161
+ # Color.each do |c|
162
+ # PieceShape.each do |s|
163
+ # Rotation.each do |r|
164
+ # [false, true].each do |f|
165
+ # (0..BOARD_SIZE-1).to_a.each do |x|
166
+ # (0..BOARD_SIZE-1).to_a.each do |y|
167
+ # moves << SetMove.new(Piece.new(c, s, r, f, Coordinates.new(x, y)))
168
+ # end
169
+ # end
170
+ # end
171
+ # end
172
+ # end
173
+ # end
174
+ # moves
175
+ # end
176
+
177
+ # --- Move Validation ------------------------------------------------------------
178
+
179
+ # Prüft, ob der gegebene [Move] zulässig ist.
180
+ # @param gamestate [GameState] Der zu untersuchende Spielstand.
181
+ # @param move der zu überprüfende Zug
182
+ #
183
+ # @return ob der Zug zulässig ist
184
+ def self.valid_move?(gamestate, move)
185
+ if move.instance_of? SkipMove
186
+ !gamestate.is_first_move?
187
+ else
188
+ valid_set_move?(gamestate, move)
156
189
  end
157
- true
158
190
  end
159
191
 
160
- def self.validate_ant_move(board, move)
161
- visited_fields = [move.start]
162
- index = 0
163
- while index < visited_fields.size
164
- current_field = visited_fields[index]
165
- new_fields = accessible_neighbours_except(board, current_field, move.start).reject { |f| visited_fields.include? f }
166
- return true if new_fields.map(&:coordinates).include?(move.destination)
167
- visited_fields += new_fields
168
- index += 1
192
+ # Prüft, ob der gegebene [SetMove] zulässig ist.
193
+ # @param gamestate [GameState] der aktuelle Spielstand
194
+ # @param move [SetMove] der zu überprüfende Zug
195
+ #
196
+ # @return ob der Zug zulässig ist
197
+ def self.valid_set_move?(gamestate, move)
198
+ return false if move.piece.color != gamestate.current_color
199
+
200
+ if gamestate.is_first_move?
201
+ # on first turn, only the start piece is allowed
202
+ return false if move.piece.kind != gamestate.start_piece
203
+ # and it may only be placed in a corner
204
+ return false if move.piece.coords.none? { |it| corner?(it) }
205
+ else
206
+ # in all other turns, only unused pieces may be placed
207
+ return false unless gamestate.undeployed_pieces(move.piece.color).include?(move.piece.kind)
208
+ # and it needs to be connected to another piece of the same color
209
+ return false if move.piece.coords.none? { |it| corners_on_color?(gamestate.board, it, move.piece.color) }
169
210
  end
170
- raise InvalidMoveException.new("No path found for ant move", move)
171
- end
172
211
 
173
- def self.is_swarm_connected(board)
174
- board_fields = board.field_list.select{ |f| !f.pieces.empty? }
175
- return true if board_fields.empty?
176
- visited_fields = board_fields.take 1
177
- total_pieces = board.pieces.size
178
- index = 0
179
- while index < visited_fields.size
180
- current_field = visited_fields[index]
181
- occupied_neighbours =
182
- get_neighbours(board, current_field.coordinates)
183
- .filter { |f| !f.pieces.empty? }
184
- occupied_neighbours -= visited_fields
185
- visited_fields += occupied_neighbours
186
- return true if visited_fields.sum{ |f| f.pieces.size } == total_pieces
187
- index += 1
212
+ # all parts of the piece need to be
213
+ move.piece.coords.each do |it|
214
+ # - on the board
215
+ return false unless gamestate.board.in_bounds?(it)
216
+ # - on a empty field
217
+ return false if obstructed?(gamestate.board, it)
218
+ # - not next to a field occupied by the same color
219
+ return false if borders_on_color?(gamestate.board, it, move.piece.color)
188
220
  end
189
- false
190
- end
191
221
 
192
- def self.validate_beetle_move(board, move)
193
- validate_destination_next_to_start(move)
194
- if ((shared_neighbours_of_two_coords(board, move.start, move.destination) + [board.field_at(move.destination), board.field_at(move.start)]).all? { |f| f.pieces.empty? })
195
- raise InvalidMoveException.new("Beetle has to move along swarm", move)
196
- end
222
+ true
197
223
  end
198
224
 
199
- def self.validate_destination_next_to_start(move)
200
- if (!is_neighbour(move.start, move.destination))
201
- raise InvalidMoveException.new("Destination field is not next to start field", move)
225
+ # Überprüft, ob das gegebene Feld ein Nachbarfeld mit der Farbe [color] hat
226
+ # @param board [Board] Das aktuelle Spielbrett
227
+ # @param field [Field] Das zu überprüfende Feld
228
+ # @param color [Color] Nach der zu suchenden Farbe
229
+ def self.borders_on_color?(board, position, color)
230
+ [Coordinates.new(1, 0), Coordinates.new(0, 1), Coordinates.new(-1, 0), Coordinates.new(0, -1)].any? do |it|
231
+ if board.in_bounds?(position + it)
232
+ board[position + it].color == color
233
+ else
234
+ false
235
+ end
202
236
  end
203
237
  end
204
238
 
205
- def self.is_neighbour(start, destination)
206
- Direction.map do |d|
207
- d.translate(start)
208
- end.include?(destination)
209
- end
210
-
211
- def self.shared_neighbours_of_two_coords(board, first_coords, second_coords)
212
- get_neighbours(board, first_coords) & get_neighbours(board, second_coords)
213
- end
214
-
215
- def self.validate_bee_move(board, move)
216
- validate_destination_next_to_start(move)
217
- if (!can_move_between(board, move.start, move.destination))
218
- raise InvalidMoveException.new("There is no path to your destination", move)
239
+ # Überprüft, ob das gegebene Feld ein diagonales Nachbarfeld mit der Farbe [color] hat
240
+ # @param board [Board] Das aktuelle Spielbrett
241
+ # @param position [Field] Das zu überprüfende Feld
242
+ # @param color [Color] Nach der zu suchenden Farbe
243
+ def self.corners_on_color?(board, position, color)
244
+ [Coordinates.new(1, 1), Coordinates.new(1, -1), Coordinates.new(-1, -1), Coordinates.new(-1, 1)].any? do |it|
245
+ board.in_bounds?(position + it) && board[position + it].color == color
219
246
  end
220
247
  end
221
248
 
222
- def self.can_move_between(board, coords1, coords2)
223
- shared = shared_neighbours_of_two_coords(board, coords1, coords2)
224
- (shared.size == 1 || shared.any? { |n| n.empty? && !n.obstructed }) && shared.any? { |n| !n.pieces.empty? }
249
+ # Überprüft, ob die gegebene [position] an einer Ecke des Boards liegt.
250
+ # @param position [Coordinates] Die zu überprüfenden Koordinaten
251
+ def self.corner?(position)
252
+ corner = [
253
+ Coordinates.new(0, 0),
254
+ Coordinates.new(BOARD_SIZE - 1, 0),
255
+ Coordinates.new(0, BOARD_SIZE - 1),
256
+ Coordinates.new(BOARD_SIZE - 1, BOARD_SIZE - 1)
257
+ ]
258
+ corner.include? position
225
259
  end
226
260
 
227
- def self.validate_grasshopper_move(board, move)
228
- if (!two_fields_on_one_straight(move.start, move.destination))
229
- raise InvalidMoveException.new("Grasshopper can only move straight lines", move)
230
- end
231
- if (is_neighbour(move.start, move.destination))
232
- raise InvalidMoveException.new("Grasshopper has to jump over at least one piece", move)
233
- end
234
- if (get_line_between_coords(board, move.start, move.destination).any? { |f| f.empty? })
235
- raise InvalidMoveException.new("Grasshopper can only jump over occupied fields, not empty ones", move)
236
- end
261
+ # Überprüft, ob die gegebene [position] schon mit einer Farbe belegt wurde.
262
+ # @param board [Board] Das aktuelle Spielbrett
263
+ # @param position [Coordinates] Die zu überprüfenden Koordinaten
264
+ def self.obstructed?(board, position)
265
+ !board[position].color.nil?
237
266
  end
238
267
 
239
- def self.two_fields_on_one_straight(coords1, coords2)
240
- return coords1.x == coords2.x || coords1.y == coords2.y || coords1.z == coords2.z
241
- end
242
-
243
- def self.get_line_between_coords(board, start, destination)
244
- if (!two_fields_on_one_straight(start, destination))
245
- raise InvalidMoveException.new("destination is not in line with start")
246
- end
268
+ # --- Perform Move ------------------------------------------------------------
247
269
 
248
- # TODO use Direction shift
249
- dX = start.x - destination.x
250
- dY = start.y - destination.y
251
- dZ = start.z - destination.z
252
- d = (dX == 0) ? dY.abs : dX.abs
253
- (1..(d-1)).to_a.map do |i|
254
- board.field_at(
255
- CubeCoordinates.new(
256
- destination.x + i * (dX <=> 0),
257
- destination.y + i * (dY <=> 0),
258
- destination.z + i * (dZ <=> 0)
259
- )
260
- )
261
- end
262
- end
263
- def self.accessible_neighbours_except(board, start, except)
264
- get_neighbours(board, start).filter do |neighbour|
265
- neighbour.empty? && can_move_between_except(board, start, neighbour, except) && neighbour.coordinates != except
266
- end
267
- end
270
+ # Führe den gegebenen [Move] im gebenenen [GameState] aus.
271
+ # @param gamestate [GameState] der aktuelle Spielstand
272
+ # @param move der auszuführende Zug
273
+ def self.perform_move(gamestate, move)
274
+ raise 'Invalid move!' unless valid_move?(gamestate, move)
268
275
 
269
- def self.can_move_between_except(board, coords1, coords2, except)
270
- shared = shared_neighbours_of_two_coords(board, coords1, coords2).reject do |f|
271
- f.pieces.size == 1 && except == f.coordinates
272
- end
273
- (shared.size == 1 || shared.any? { |s| s.empty? && !s.obstructed }) && shared.any? { |s| !s.pieces.empty? }
274
- end
276
+ if move.instance_of? SetMove
277
+ gamestate.undeployed_pieces(move.piece.color).delete(move.piece)
275
278
 
276
- def self.validate_spider_move(board, move)
277
- found = get_accessible_neighbours(board, move.start).any? do |depth_one|
278
- get_accessible_neighbours_except(board, depth_one, move.start).any? do |depth_two|
279
- get_accessible_neighbours_except(board, depth_two, move.start).reject{ |f| f.coordinates == depth_one.coordinates }.any? { |f| move.destination == f.coordinates }
279
+ # Apply piece to board
280
+ move.piece.coords.each do |coord|
281
+ gamestate.board[coord].color = move.piece.color
280
282
  end
281
- end
282
- return true if (found)
283
- raise InvalidMoveException.new("No path found for spider move", move)
284
- end
285
283
 
286
- def self.get_accessible_neighbours(board, start)
287
- get_neighbours(board, start).filter do |neighbour|
288
- neighbour.empty? && can_move_between(board, start, neighbour)
289
- end
290
- end
291
-
292
- def self.get_accessible_neighbours_except(board, start, except)
293
- get_neighbours(board, start).filter do |neighbour|
294
- neighbour.empty? &&
295
- can_move_between_except(board, start, neighbour, except) &&
296
- neighbour.coordinates != except
297
- end
298
- end
299
-
300
- def self.perform_move(gamestate, move)
301
- raise "Invalid move!" unless valid_move?(gamestate, move)
302
- case move
303
- when SetMove
304
- # delete first occurrence of piece
305
- gamestate.undeployed_pieces(move.piece.color).delete_at(
306
- gamestate.undeployed_pieces(move.piece.color).index(move.piece) ||
307
- gamestate.undeployed_pieces(move.piece.color).length
308
- )
309
- gamestate.board.field_at(move.destination).add_piece(move.piece)
310
- when DragMove
311
- piece_to_move = gamestate.board.field_at(move.start).remove_piece
312
- gamestate.board.field_at(move.destination).add_piece(piece_to_move)
284
+ # If it was the last piece for this color, remove it from the turn queue
285
+ if gamestate.undeployed_pieces(move.piece.color).empty?
286
+ gamestate.lastMoveMono += move.color to(move.piece.kind == PieceShape.MONO)
287
+ gamestate.remove_active_color
288
+ end
313
289
  end
314
290
  gamestate.turn += 1
291
+ gamestate.round += 1
315
292
  gamestate.last_move = move
316
293
  end
317
294
 
318
- # all possible moves, but will *not* return the skip move if no other moves are possible!
319
- def self.possible_moves(gamestate)
320
- possible_set_moves(gamestate) + possible_drag_moves(gamestate)
321
- end
295
+ # --- Other ------------------------------------------------------------
322
296
 
323
- def self.possible_drag_moves(gamestate)
324
- gamestate.board.fields_of_color(gamestate.current_player_color).flat_map do |start_field|
325
- edge_targets = empty_fields_connected_to_swarm(gamestate.board)
326
- additional_targets =
327
- if start_field.pieces.last.type == PieceType::BEETLE
328
- get_neighbours(gamestate.board, start_field).uniq
329
- else
330
- []
331
- end
332
- edge_targets + additional_targets.map do |destination|
333
- move = DragMove.new(start_field, destination)
334
- begin
335
- valid_move?(gamestate, move)
336
- move
337
- rescue InvalidMoveException
338
- nil
339
- end
340
- end.compact
297
+ # Berechne den Punktestand anhand der gegebenen [PieceShape]s.
298
+ # @param undeployed eine Sammlung aller nicht gelegten [PieceShape]s
299
+ # @param monoLast ob der letzte gelegte Stein das Monomino war
300
+ #
301
+ # @return die erreichte Punktezahl
302
+ def self.get_points_from_undeployed(undeployed, mono_last = false)
303
+ # If all pieces were placed:
304
+ if undeployed.empty?
305
+ # Return sum of all squares plus 15 bonus points
306
+ return SUM_MAX_SQUARES + 15 +
307
+ # If the Monomino was the last placed piece, add another 5 points
308
+ mono_last ? 5 : 0
341
309
  end
310
+ # One point per block per piece placed
311
+ SUM_MAX_SQUARES - undeployed.map(&:size).sum
342
312
  end
343
313
 
344
- def self.empty_fields_connected_to_swarm(board)
345
- board.field_list
346
- .filter { |f| f.has_owner }
347
- .flat_map { |f| get_neighbours(board, f).filter { f.empty? } }
348
- .uniq
349
- end
350
-
351
- def self.possible_set_move_destinations(board, owner)
352
- board.fields_of_color(owner)
353
- .flat_map { |f| get_neighbours(board, f).filter { |f| f.empty? } }
354
- .uniq
355
- .filter { |f| get_neighbours(board, f).all? { |n| n.color != owner.opponent } }
314
+ # Gibt einen zufälligen Pentomino zurück, welcher nicht `x` ist.
315
+ def self.get_random_pentomino
316
+ PieceShape.map(&:value).select { |it| it.size == 5 && it != PieceShape::PENTO_X }
356
317
  end
357
318
 
358
- def self.possible_set_moves(gamestate)
359
- undeployed = gamestate.undeployed_pieces(gamestate.current_player_color)
360
- set_destinations =
361
- if (undeployed.size == STARTING_PIECES.size)
362
- # current player has not placed any pieces yet (first or second turn)
363
- if (gamestate.undeployed_pieces(gamestate.other_player_color).size == STARTING_PIECES.size)
364
- # other player also has not placed any pieces yet (first turn, all destinations allowed (except obstructed)
365
- gamestate.board.field_list.filter { |f| f.empty? }
366
- else
367
- # other player placed a piece already
368
- gamestate.board
369
- .fields_of_color(gamestate.other_player_color)
370
- .flat_map do |f|
371
- GameRuleLogic.get_neighbours(gamestate.board, f).filter(&:empty?)
372
- end
373
- end
374
- else
375
- possible_set_move_destinations(gamestate.board, gamestate.current_player_color)
376
- end
319
+ # Entferne alle Farben, die keine Steine mehr auf dem Feld platzieren können.
320
+ def remove_invalid_colors(gamestate)
321
+ return if gamestate.ordered_colors.empty?
322
+ return unless get_possible_moves(gamestate).empty?
377
323
 
378
- possible_piece_types =
379
- if (!has_player_placed_bee(gamestate) && gamestate.turn > 5)
380
- [PieceType::BEE]
381
- else
382
- undeployed.map(&:type).uniq
383
- end
384
- set_destinations
385
- .flat_map do |d|
386
- possible_piece_types.map do |u|
387
- SetMove.new(Piece.new(gamestate.current_player_color, u), d)
388
- end
389
- end
324
+ gamestate.remove_active_color
325
+ remove_invalid_colors(gamestate)
390
326
  end
391
327
 
392
328
  # Prueft, ob ein Spieler im gegebenen GameState gewonnen hat.
393
329
  # @param gamestate [GameState] Der zu untersuchende GameState.
394
330
  # @return [Condition] nil, if the game is not won or a Condition indicating the winning player
395
- def self.winning_condition(gamestate)
396
- raise "Not implemented yet!"
397
- winner_by_single_swarm = [PlayerColor::RED, PlayerColor::BLUE].select do |player_color|
398
- GameRuleLogic.swarm_size(gamestate.board, player_color) ==
399
- gamestate.board.fields_of_type(PlayerColor.field_type(player_color)).size
400
- end
401
- if winner_by_single_swarm.any? && gamestate.turn.even?
402
- return Condition.new(nil, "Unentschieden.") if winner_by_single_swarm.size == 2
403
- return Condition.new(winner_by_single_swarm.first, "Schwarm wurde vereint.")
404
- end
405
- player_with_biggest_swarm = [PlayerColor::RED, PlayerColor::BLUE].sort_by do |player_color|
406
- GameRuleLogic.swarm_size(gamestate.board, player_color)
407
- end.reverse.first
408
- return Condition.new(player_with_biggest_swarm, "Rundenlimit erreicht, Schwarm mit den meisten Fischen gewinnt.") if gamestate.turn == 60
409
- nil
331
+ def self.winning_condition(_gamestate)
332
+ raise 'Not implemented yet!'
410
333
  end
411
334
  end