software_challenge_client 20.2.4 → 21.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 (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