software_challenge_client 20.2.3 → 21.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +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 +64 -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 +82 -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,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
|