software_challenge_client 20.2.2 → 21.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -4
- 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 +99 -34
- 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 +4 -1
- 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 -53
- data/lib/software_challenge_client/game_rule_logic.rb +257 -332
- data/lib/software_challenge_client/game_state.rb +86 -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 +43 -14
- 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 +83 -75
- 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 -23
- data/lib/software_challenge_client/direction.rb +0 -55
- data/lib/software_challenge_client/drag_move.rb +0 -19
- 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.
|
@@ -12,12 +13,14 @@ class Condition
|
|
12
13
|
attr_reader :reason
|
13
14
|
|
14
15
|
# Initializes the winning Condition with a player
|
15
|
-
# @param
|
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,37 +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
|
-
#
|
4
|
+
# Ein Feld des Spielfelds. Ein Spielfeld ist durch die Koordinaten eindeutig
|
5
|
+
# identifiziert.
|
5
6
|
class Field
|
6
|
-
# @!attribute [rw]
|
7
|
-
# @return [
|
8
|
-
|
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 [
|
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
|
-
#
|
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
|
22
|
-
|
23
|
-
|
24
|
-
@
|
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
|
30
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
87
|
+
moves
|
99
88
|
end
|
100
89
|
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
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
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
117
|
-
|
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
|
-
|
121
|
-
|
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
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
131
|
-
|
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
|
-
|
135
|
-
|
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
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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
|
-
|
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)
|
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
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
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
|
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
|
-
|
240
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
270
|
-
|
271
|
-
|
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
|
-
|
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 }
|
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
|
-
|
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)
|
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
|
-
#
|
319
|
-
def self.possible_moves(gamestate)
|
320
|
-
possible_set_moves(gamestate) + possible_drag_moves(gamestate)
|
321
|
-
end
|
297
|
+
# --- Other ------------------------------------------------------------
|
322
298
|
|
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
|
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
|
-
|
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 } }
|
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
|
-
|
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
|
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
|
-
|
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
|
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(
|
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
|
333
|
+
def self.winning_condition(_gamestate)
|
334
|
+
raise 'Not implemented yet!'
|
410
335
|
end
|
411
336
|
end
|