software_challenge_client 20.2.2 → 21.0.2

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 -4
  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 +99 -34
  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 +4 -1
  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 -53
  23. data/lib/software_challenge_client/game_rule_logic.rb +257 -332
  24. data/lib/software_challenge_client/game_state.rb +86 -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 +43 -14
  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 +83 -75
  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 -23
  44. data/lib/software_challenge_client/direction.rb +0 -55
  45. data/lib/software_challenge_client/drag_move.rb +0 -19
  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.
@@ -12,12 +13,14 @@ class Condition
12
13
  attr_reader :reason
13
14
 
14
15
  # Initializes the winning Condition with a player
15
- # @param winer [Player] winning player
16
+ # @param winner [Player] winning player
17
+ # @param reason [String] why the player has won
16
18
  def initialize(winner, reason)
17
19
  @winner = winner
18
20
  @reason = reason
19
21
  end
20
22
 
23
+ # Überprüfe ob es ein Unentschieden gab
21
24
  def draw?
22
25
  @winner.nil?
23
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,37 +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
- # Das type Attribut gibt an, um welchen Feldtyp es sich handelt
4
+ # Ein Feld des Spielfelds. Ein Spielfeld ist durch die Koordinaten eindeutig
5
+ # identifiziert.
5
6
  class Field
6
- # @!attribute [rw] pieces
7
- # @return [Array<Piece>] Spielsteine auf dem Feld, beginnend beim untersten Stein
8
- attr_accessor :pieces
7
+ # @!attribute [rw] color
8
+ # @return [Color] Farbe des überdeckenden Spielsteins, falls vorhanden, sonst
9
+ # nil
10
+ attr_accessor :color
9
11
  # @!attribute [r] coordinates
10
- # @return [CubeCoordinates] die Cube-Coordinates des Feldes
12
+ # @return [Coordinates] die X-Y-Koordinaten des Feldes
11
13
  attr_reader :coordinates
12
- # @!attribute [r] obstructed
13
- # @return [Boolean] ob das Feld durch eine Brombeere blockiert ist
14
- attr_reader :obstructed
15
14
 
16
- # Konstruktor
15
+ # Erstellt ein neues leeres Feld.
17
16
  #
18
- # @param type [FieldType] Feldtyp
19
17
  # @param x [Integer] X-Koordinate
20
18
  # @param y [Integer] Y-Koordinate
21
- # @param pieces [Array<Piece>] Spielsteine auf dem Feld
22
- # @param obstructed [Boolean] Ob das Feld blockiert ist (Brombeere)
23
- def initialize(x, y, pieces = [], obstructed = false)
24
- @pieces = pieces
25
- @coordinates = CubeCoordinates.new(x, y)
26
- @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)
27
23
  end
28
24
 
29
- # Vergleicht zwei Felder. Felder sind gleich, wenn sie gleiche Koordinaten und gleichen Typ haben.
30
- # @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.
31
28
  def ==(other)
32
29
  coordinates == other.coordinates &&
33
- obstructed == other.obstructed &&
34
- pieces == other.pieces
30
+ color == other.color
35
31
  end
36
32
 
37
33
  def x
@@ -42,41 +38,13 @@ class Field
42
38
  coordinates.y
43
39
  end
44
40
 
45
- def z
46
- coordinates.z
47
- end
48
-
41
+ # @return [Boolean] true, wenn das Feld nicht durch einen Spielstein überdeckt ist, sonst false
49
42
  def empty?
50
- pieces.empty? && !obstructed
51
- end
52
-
53
- def obstructed?
54
- obstructed
55
- end
56
-
57
- def add_piece(piece)
58
- pieces.push(piece)
59
- end
60
-
61
- def remove_piece
62
- pieces.pop
63
- end
64
-
65
- def color
66
- pieces.last&.color
67
- end
68
-
69
- def has_owner
70
- !color.nil?
43
+ color.nil?
71
44
  end
72
45
 
73
46
  # @return [String] Textuelle Darstellung des Feldes.
74
47
  def to_s
75
- s = "Feld #{coordinates}, "
76
- if obstructed?
77
- s += 'blockiert'
78
- else
79
- s += "Steine: #{pieces.map(&:to_s).join(', ')}"
80
- end
48
+ empty? ? '_' : color.value
81
49
  end
82
50
  end
@@ -1,411 +1,336 @@
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 Piranhas 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)
73
+ moves.select { |m| valid_set_move?(gamestate, m) }.to_a
74
+ end
75
+
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))
96
85
  end
97
86
  end
98
- true
87
+ moves
99
88
  end
100
89
 
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)
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
104
97
  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)
107
- end
108
- true
98
+ moves
109
99
  end
110
100
 
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)
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
114
117
  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
+ 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
118
138
  end
119
-
120
- if (gamestate.board.field_at(move.start).pieces.empty?)
121
- raise InvalidMoveException.new("There is no piece to move", 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)
122
189
  end
190
+ end
123
191
 
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
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
+ # Check whether the color's move is currently active
199
+ return false if move.piece.color != gamestate.current_color
129
200
 
130
- if (move.start == move.destination)
131
- raise InvalidMoveException.new("Destination and start are equal", move)
201
+ # Check whether the shape is valid
202
+ if gamestate.is_first_move?
203
+ return false if move.piece.kind != gamestate.start_piece
204
+ elsif !gamestate.undeployed_pieces(move.piece.color).include?(move.piece.kind)
205
+ return false
132
206
  end
133
207
 
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)
208
+ # Check whether the piece can be placed
209
+ move.piece.coords.each do |it|
210
+ return false unless gamestate.board.in_bounds?(it)
211
+ return false if obstructed?(gamestate.board, it)
212
+ return false if borders_on_color?(gamestate.board, it, move.piece.color)
136
213
  end
137
214
 
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)
215
+ if gamestate.is_first_move?
216
+ # Check if it is placed correctly in a corner
217
+ return false if move.piece.coords.none? { |it| corner?(it) }
218
+ else
219
+ # Check if the piece is connected to at least one tile of same color by corner
220
+ return false if move.piece.coords.none? { |it| corners_on_color?(gamestate.board, it, move.piece.color) }
143
221
  end
144
222
 
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)
156
- end
157
223
  true
158
224
  end
159
225
 
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
169
- end
170
- raise InvalidMoveException.new("No path found for ant move", move)
171
- end
172
-
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
188
- end
189
- false
190
- end
191
-
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
197
- end
198
-
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)
226
+ # Überprüft, ob das gegebene Feld ein Nachbarfeld mit der Farbe [color] hat
227
+ # @param board [Board] Das aktuelle Spielbrett
228
+ # @param field [Field] Das zu überprüfende Feld
229
+ # @param color [Color] Nach der zu suchenden Farbe
230
+ def self.borders_on_color?(board, position, color)
231
+ [Coordinates.new(1, 0), Coordinates.new(0, 1), Coordinates.new(-1, 0), Coordinates.new(0, -1)].any? do |it|
232
+ if board.in_bounds?(position + it)
233
+ board[position + it].color == color
234
+ else
235
+ false
236
+ end
202
237
  end
203
238
  end
204
239
 
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)
240
+ # Überprüft, ob das gegebene Feld ein diagonales Nachbarfeld mit der Farbe [color] hat
241
+ # @param board [Board] Das aktuelle Spielbrett
242
+ # @param position [Field] Das zu überprüfende Feld
243
+ # @param color [Color] Nach der zu suchenden Farbe
244
+ def self.corners_on_color?(board, position, color)
245
+ [Coordinates.new(1, 1), Coordinates.new(1, -1), Coordinates.new(-1, -1), Coordinates.new(-1, 1)].any? do |it|
246
+ board.in_bounds?(position + it) && board[position + it].color == color
219
247
  end
220
248
  end
221
249
 
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? }
225
- end
226
-
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
250
+ # Überprüft, ob die gegebene [position] an einer Ecke des Boards liegt.
251
+ # @param position [Coordinates] Die zu überprüfenden Koordinaten
252
+ def self.corner?(position)
253
+ corner = [
254
+ Coordinates.new(0, 0),
255
+ Coordinates.new(BOARD_SIZE - 1, 0),
256
+ Coordinates.new(0, BOARD_SIZE - 1),
257
+ Coordinates.new(BOARD_SIZE - 1, BOARD_SIZE - 1)
258
+ ]
259
+ corner.include? position
237
260
  end
238
261
 
239
- def self.two_fields_on_one_straight(coords1, coords2)
240
- return coords1.x == coords2.x || coords1.y == coords2.y || coords1.z == coords2.z
262
+ # Überprüft, ob die gegebene [position] schon mit einer Farbe belegt wurde.
263
+ # @param board [Board] Das aktuelle Spielbrett
264
+ # @param position [Coordinates] Die zu überprüfenden Koordinaten
265
+ def self.obstructed?(board, position)
266
+ !board[position].color.nil?
241
267
  end
242
268
 
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
269
+ # --- Perform Move ------------------------------------------------------------
247
270
 
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
271
+ # Führe den gegebenen [Move] im gebenenen [GameState] aus.
272
+ # @param gamestate [GameState] der aktuelle Spielstand
273
+ # @param move der auszuführende Zug
274
+ def self.perform_move(gamestate, move)
275
+ raise 'Invalid move!' unless valid_move?(gamestate, move)
268
276
 
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
277
+ if move.instance_of? SetMove
278
+ gamestate.undeployed_pieces(move.piece.color).delete(move.piece)
279
+ # gamestate.deployed_pieces(move.piece.color).add(move.piece)
275
280
 
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 }
281
+ # Apply piece to board
282
+ move.piece.coords.each do |coord|
283
+ gamestate.board[coord].color = move.piece.color
280
284
  end
281
- end
282
- return true if (found)
283
- raise InvalidMoveException.new("No path found for spider move", move)
284
- end
285
285
 
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)
286
+ # If it was the last piece for this color, remove it from the turn queue
287
+ if gamestate.undeployed_pieces(move.piece.color).empty?
288
+ gamestate.lastMoveMono += move.color to(move.piece.kind == PieceShape.MONO)
289
+ gamestate.remove_active_color
290
+ end
313
291
  end
314
292
  gamestate.turn += 1
293
+ gamestate.round += 1
315
294
  gamestate.last_move = move
316
295
  end
317
296
 
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
297
+ # --- Other ------------------------------------------------------------
322
298
 
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
299
+ # Berechne den Punktestand anhand der gegebenen [PieceShape]s.
300
+ # @param undeployed eine Sammlung aller nicht gelegten [PieceShape]s
301
+ # @param monoLast ob der letzte gelegte Stein das Monomino war
302
+ #
303
+ # @return die erreichte Punktezahl
304
+ def self.get_points_from_undeployed(undeployed, mono_last = false)
305
+ # If all pieces were placed:
306
+ if undeployed.empty?
307
+ # Return sum of all squares plus 15 bonus points
308
+ return SUM_MAX_SQUARES + 15 +
309
+ # If the Monomino was the last placed piece, add another 5 points
310
+ mono_last ? 5 : 0
341
311
  end
312
+ # One point per block per piece placed
313
+ SUM_MAX_SQUARES - undeployed.map(&:size).sum
342
314
  end
343
315
 
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 } }
316
+ # Gibt einen zufälligen Pentomino zurück, welcher nicht `x` ist.
317
+ def self.get_random_pentomino
318
+ PieceShape.map(&:value).select { |it| it.size == 5 && it != PieceShape::PENTO_X }
356
319
  end
357
320
 
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
321
+ # Entferne alle Farben, die keine Steine mehr auf dem Feld platzieren können.
322
+ def remove_invalid_colors(gamestate)
323
+ return if gamestate.ordered_colors.empty?
324
+ return unless get_possible_moves(gamestate).empty?
377
325
 
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
326
+ gamestate.remove_active_color
327
+ remove_invalid_colors(gamestate)
390
328
  end
391
329
 
392
330
  # Prueft, ob ein Spieler im gegebenen GameState gewonnen hat.
393
331
  # @param gamestate [GameState] Der zu untersuchende GameState.
394
332
  # @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
333
+ def self.winning_condition(_gamestate)
334
+ raise 'Not implemented yet!'
410
335
  end
411
336
  end