software_challenge_client 20.2.0 → 21.0.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 +16 -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 +23 -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 +255 -328
  24. data/lib/software_challenge_client/game_state.rb +87 -69
  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 +83 -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 +82 -76
  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 +14 -12
  42. metadata +52 -35
  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,23 @@
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
+ # Implementiert den [] Operator für diese Klasse
13
+ class << self
14
+ def [](digit)
15
+ constants.find { |const| const_get(const) == digit }
16
+ end
17
+ end
18
+
19
+ # Gibt den color namen zurück
20
+ def to_s
21
+ self.key.to_s
22
+ end
23
+ 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,407 +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 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.filter { |m| valid_set_move?(gamestate, m) }.to_a
74
+ end
75
+
76
+ # Helper method to calculate all transformations of one shape on one spot
77
+ def self.moves_for_shape_on(color, shape, position)
78
+ moves = Set[]
79
+ Rotation.each do |r|
80
+ [true, false].each do |f|
81
+ moves << SetMove.new(Piece.new(color, shape, r, f, position))
96
82
  end
97
83
  end
98
- true
84
+ moves
99
85
  end
100
86
 
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)
87
+ # Gib eine Liste aller möglichen Legezüge zurück, auch wenn es die erste Runde ist.
88
+ def self.all_possible_setmoves(gamestate)
89
+ moves = []
90
+ fields = valid_fields(gamestate)
91
+ gamestate.undeployed_pieces(gamestate.current_color).each do |p|
92
+ (moves << possible_moves_for_shape(gamestate, p, fields)).flatten
104
93
  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
94
+ moves
109
95
  end
110
96
 
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)
97
+ # Gibt eine Liste aller möglichen SetMoves für diese Form zurück.
98
+ # @param gamestate Der aktuelle Spielstand
99
+ # @param shape Die [PieceShape], die die Züge nutzen sollen
100
+ #
101
+ # @return Alle möglichen Züge mit der Form
102
+ def self.possible_moves_for_shape(gamestate, shape, fields = valid_fields(gamestate))
103
+ color = gamestate.current_color
104
+
105
+ moves = Set[]
106
+ fields.each do |field|
107
+ Rotation.each do |r|
108
+ [true, false].each do |f|
109
+ piece = Piece.new(color, shape, r, f, Coordinates.new(0, 0))
110
+ piece.coords.each do |pos|
111
+ moves << SetMove.new(Piece.new(color, shape, r, f, Coordinates.new(field.x - pos.x, field.y - pos.y)))
112
+ end
113
+ end
114
+ end
114
115
  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)
116
+ moves.filter { |m| valid_set_move?(gamestate, m) }.to_a
117
+ end
118
+
119
+ # Gibt eine Liste aller Felder zurück, an denen möglicherweise Züge gemacht werden kann.
120
+ # @param gamestate Der aktuelle Spielstand
121
+ def self.valid_fields(gamestate)
122
+ color = gamestate.current_color
123
+ board = gamestate.board
124
+ fields = Set[]
125
+ board.fields_of_color(color).each do |field|
126
+ [Coordinates.new(field.x - 1, field.y - 1),
127
+ Coordinates.new(field.x - 1, field.y + 1),
128
+ Coordinates.new(field.x + 1, field.y - 1),
129
+ Coordinates.new(field.x + 1, field.y + 1)].each do |corner|
130
+ next unless Board.contains(corner)
131
+ next unless board[corner].empty?
132
+ next if neighbor_of_color?(board, Field.new(corner.x, corner.y), color)
133
+
134
+ fields << corner
135
+ end
118
136
  end
119
-
120
- if (gamestate.board.field_at(move.start).pieces.empty?)
121
- raise InvalidMoveException.new("There is no piece to move", move)
137
+ fields
138
+ end
139
+
140
+ # Überprüft, ob das gegebene Feld ein Nachbarfeld mit der Farbe [color] hat
141
+ # @param board Das aktuelle Board
142
+ # @param field Das zu überprüfende Feld
143
+ # @param color Nach der zu suchenden Farbe
144
+ def self.neighbor_of_color?(board, field, color)
145
+ [Coordinates.new(field.x - 1, field.y),
146
+ Coordinates.new(field.x, field.y - 1),
147
+ Coordinates.new(field.x + 1, field.y),
148
+ Coordinates.new(field.x, field.y + 1)].any? do |neighbor|
149
+ Board.contains(neighbor) && board[neighbor].color == color
150
+ end
151
+ end
152
+
153
+ # # Return a list of all moves, impossible or not.
154
+ # # There's no real usage, except maybe for cases where no Move validation happens
155
+ # # if `Constants.VALIDATE_MOVE` is false, then this function should return the same
156
+ # # Set as `::getPossibleMoves`
157
+ # def self.get_all_set_moves()
158
+ # moves = []
159
+ # Color.each do |c|
160
+ # PieceShape.each do |s|
161
+ # Rotation.each do |r|
162
+ # [false, true].each do |f|
163
+ # (0..BOARD_SIZE-1).to_a.each do |x|
164
+ # (0..BOARD_SIZE-1).to_a.each do |y|
165
+ # moves << SetMove.new(Piece.new(c, s, r, f, Coordinates.new(x, y)))
166
+ # end
167
+ # end
168
+ # end
169
+ # end
170
+ # end
171
+ # end
172
+ # moves
173
+ # end
174
+
175
+ # --- Move Validation ------------------------------------------------------------
176
+
177
+ # Prüft, ob der gegebene [Move] zulässig ist.
178
+ # @param gamestate der aktuelle Spielstand
179
+ # @param move der zu überprüfende Zug
180
+ #
181
+ # @return ob der Zug zulässig ist
182
+ def self.valid_move?(gamestate, move)
183
+ if move.instance_of? SkipMove
184
+ !gamestate.is_first_move?
185
+ else
186
+ valid_set_move?(gamestate, move)
122
187
  end
188
+ end
123
189
 
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
190
+ # Prüft, ob der gegebene [SetMove] zulässig ist.
191
+ # @param gamestate [GameState] der aktuelle Spielstand
192
+ # @param move [SetMove] der zu überprüfende Zug
193
+ #
194
+ # @return ob der Zug zulässig ist
195
+ def self.valid_set_move?(gamestate, move)
196
+ # Check whether the color's move is currently active
197
+ return false if move.piece.color != gamestate.current_color
129
198
 
130
- if (move.start == move.destination)
131
- raise InvalidMoveException.new("Destination and start are equal", move)
199
+ # Check whether the shape is valid
200
+ if gamestate.is_first_move?
201
+ return false if move.piece.kind != gamestate.start_piece
202
+ elsif !gamestate.undeployed_pieces(move.piece.color).include?(move.piece.kind)
203
+ return false
132
204
  end
133
205
 
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)
206
+ # Check whether the piece can be placed
207
+ move.piece.coords.each do |it|
208
+ return false unless gamestate.board.in_bounds?(it)
209
+ return false if obstructed?(gamestate.board, it)
210
+ return false if borders_on_color?(gamestate.board, it, move.piece.color)
136
211
  end
137
212
 
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)
213
+ if gamestate.is_first_move?
214
+ # Check if it is placed correctly in a corner
215
+ return false if move.piece.coords.none? { |it| corner?(it) }
216
+ else
217
+ # Check if the piece is connected to at least one tile of same color by corner
218
+ return false if move.piece.coords.none? { |it| corners_on_color?(gamestate.board, it, move.piece.color) }
143
219
  end
144
220
 
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
221
  true
158
222
  end
159
223
 
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)
224
+ # Überprüft, ob das gegebene Feld ein Nachbarfeld mit der Farbe [color] hat
225
+ # @param board [Board] Das aktuelle Spielbrett
226
+ # @param field [Field] Das zu überprüfende Feld
227
+ # @param color [Color] Nach der zu suchenden Farbe
228
+ def self.borders_on_color?(board, position, color)
229
+ [Coordinates.new(1, 0), Coordinates.new(0, 1), Coordinates.new(-1, 0), Coordinates.new(0, -1)].any? do |it|
230
+ if board.in_bounds?(position + it)
231
+ board[position + it].color == color
232
+ else
233
+ false
234
+ end
202
235
  end
203
236
  end
204
237
 
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)
238
+ # Überprüft, ob das gegebene Feld ein diagonales Nachbarfeld mit der Farbe [color] hat
239
+ # @param board [Board] Das aktuelle Spielbrett
240
+ # @param field Das zu überprüfende [Field]
241
+ # @param color Nach der zu suchenden [Color]
242
+ def self.corners_on_color?(board, position, color)
243
+ [Coordinates.new(1, 1), Coordinates.new(1, -1), Coordinates.new(-1, -1), Coordinates.new(-1, 1)].any? do |it|
244
+ board.in_bounds?(position + it) && board[position + it].color == color
219
245
  end
220
246
  end
221
247
 
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
248
+ # Überprüft, ob die gegebene [position] an einer Ecke des Boards liegt.
249
+ # @param position [Coordinates] Die zu überprüfenden Koordinaten
250
+ def self.corner?(position)
251
+ corner = [
252
+ Coordinates.new(0, 0),
253
+ Coordinates.new(BOARD_SIZE - 1, 0),
254
+ Coordinates.new(0, BOARD_SIZE - 1),
255
+ Coordinates.new(BOARD_SIZE - 1, BOARD_SIZE - 1)
256
+ ]
257
+ corner.include? position
237
258
  end
238
259
 
239
- def self.two_fields_on_one_straight(coords1, coords2)
240
- return coords1.x == coords2.x || coords1.y == coords2.y || coords1.z == coords2.z
260
+ # Überprüft, ob die gegebene [position] schon mit einer Farbe belegt wurde.
261
+ # @param board [Board] Das aktuelle Spielbrett
262
+ # @param position [Coordinates] Die zu überprüfenden Koordinaten
263
+ def self.obstructed?(board, position)
264
+ !board[position].color.nil?
241
265
  end
242
266
 
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
267
+ # --- Perform Move ------------------------------------------------------------
247
268
 
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
269
+ # Führe den gegebenen [Move] im gebenenen [GameState] aus.
270
+ # @param gamestate [GameState] der aktuelle Spielstand
271
+ # @param move der auszuführende Zug
272
+ def self.perform_move(gamestate, move)
273
+ raise 'Invalid move!' unless valid_move?(gamestate, move)
268
274
 
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
275
+ if move.instance_of? SetMove
276
+ gamestate.undeployed_pieces(move.piece.color).delete(move.piece)
277
+ # gamestate.deployed_pieces(move.piece.color).add(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
- gamestate.undeployed_pieces(move.piece.color).remove(move.piece)
305
- gamestate.board.field_at(move.destination).add_piece(move.piece)
306
- when DragMove
307
- piece_to_move = gamestate.board.field_at(move.start).remove_piece
308
- 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
309
289
  end
310
290
  gamestate.turn += 1
291
+ gamestate.round += 1
311
292
  gamestate.last_move = move
312
293
  end
313
294
 
314
- # all possible moves, but will *not* return the skip move if no other moves are possible!
315
- def self.possible_moves(gamestate)
316
- possible_set_moves(gamestate) + possible_drag_moves(gamestate)
317
- end
295
+ # --- Other ------------------------------------------------------------
318
296
 
319
- def self.possible_drag_moves(gamestate)
320
- gamestate.board.fields_of_color(gamestate.current_player_color).flat_map do |start_field|
321
- edge_targets = empty_fields_connected_to_swarm(gamestate.board)
322
- additional_targets =
323
- if start_field.pieces.last.type == PieceType::BEETLE
324
- get_neighbours(gamestate.board, start_field).uniq
325
- else
326
- []
327
- end
328
- edge_targets + additional_targets.map do |destination|
329
- move = DragMove.new(start_field, destination)
330
- begin
331
- valid_move?(gamestate, move)
332
- move
333
- rescue InvalidMoveException
334
- nil
335
- end
336
- 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
337
309
  end
310
+ # One point per block per piece placed
311
+ SUM_MAX_SQUARES - undeployed.map(&:size).sum
338
312
  end
339
313
 
340
- def self.empty_fields_connected_to_swarm(board)
341
- board.field_list
342
- .filter { |f| f.has_owner }
343
- .flat_map { |f| get_neighbours(board, f).filter { f.empty? } }
344
- .uniq
345
- end
346
-
347
- def self.possible_set_move_destinations(board, owner)
348
- board.fields_of_color(owner)
349
- .flat_map { |f| get_neighbours(board, f).filter { |f| f.empty? } }
350
- .uniq
351
- .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 }
352
317
  end
353
318
 
354
- def self.possible_set_moves(gamestate)
355
- undeployed = gamestate.undeployed_pieces(gamestate.current_player_color)
356
- set_destinations =
357
- if (undeployed.size == STARTING_PIECES.size)
358
- # current player has not placed any pieces yet (first or second turn)
359
- if (gamestate.undeployed_pieces(gamestate.other_player_color).size == STARTING_PIECES.size)
360
- # other player also has not placed any pieces yet (first turn, all destinations allowed (except obstructed)
361
- gamestate.board.field_list.filter { |f| f.empty? }
362
- else
363
- # other player placed a piece already
364
- gamestate.board
365
- .fields_of_color(gamestate.other_player_color)
366
- .flat_map do |f|
367
- GameRuleLogic.get_neighbours(gamestate.board, f).filter(&:empty?)
368
- end
369
- end
370
- else
371
- possible_set_move_destinations(gamestate.board, gamestate.current_player_color)
372
- 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?
373
323
 
374
- possible_piece_types =
375
- if (!has_player_placed_bee(gamestate) && gamestate.turn > 5)
376
- [PieceType::BEE]
377
- else
378
- undeployed.map(&:type).uniq
379
- end
380
- set_destinations
381
- .flat_map do |d|
382
- possible_piece_types.map do |u|
383
- SetMove.new(Piece.new(gamestate.current_player_color, u), d)
384
- end
385
- end
324
+ gamestate.remove_active_color
325
+ remove_invalid_colors(gamestate)
386
326
  end
387
327
 
388
328
  # Prueft, ob ein Spieler im gegebenen GameState gewonnen hat.
389
329
  # @param gamestate [GameState] Der zu untersuchende GameState.
390
330
  # @return [Condition] nil, if the game is not won or a Condition indicating the winning player
391
- def self.winning_condition(gamestate)
392
- raise "Not implemented yet!"
393
- winner_by_single_swarm = [PlayerColor::RED, PlayerColor::BLUE].select do |player_color|
394
- GameRuleLogic.swarm_size(gamestate.board, player_color) ==
395
- gamestate.board.fields_of_type(PlayerColor.field_type(player_color)).size
396
- end
397
- if winner_by_single_swarm.any? && gamestate.turn.even?
398
- return Condition.new(nil, "Unentschieden.") if winner_by_single_swarm.size == 2
399
- return Condition.new(winner_by_single_swarm.first, "Schwarm wurde vereint.")
400
- end
401
- player_with_biggest_swarm = [PlayerColor::RED, PlayerColor::BLUE].sort_by do |player_color|
402
- GameRuleLogic.swarm_size(gamestate.board, player_color)
403
- end.reverse.first
404
- return Condition.new(player_with_biggest_swarm, "Rundenlimit erreicht, Schwarm mit den meisten Fischen gewinnt.") if gamestate.turn == 60
405
- nil
331
+ def self.winning_condition(_gamestate)
332
+ raise 'Not implemented yet!'
406
333
  end
407
334
  end