software_challenge_client 20.2.2 → 21.0.2

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 -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