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