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
@@ -1,4 +1,5 @@
|
|
1
1
|
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
# Ein Spieler
|
4
5
|
class Player
|
@@ -6,15 +7,15 @@ class Player
|
|
6
7
|
# @return [String] der Name des Spielers, hat keine Auswirkungen auf das Spiel
|
7
8
|
attr_reader :name
|
8
9
|
|
9
|
-
# @!attribute [r]
|
10
|
-
# @return [
|
11
|
-
attr_reader :
|
10
|
+
# @!attribute [r] type
|
11
|
+
# @return [PlayerType] erster (PlayerType::ONE) oder zweiter (PlayerType::TWO) Spieler
|
12
|
+
attr_reader :type
|
12
13
|
|
13
14
|
# Konstruktor
|
14
|
-
# @param
|
15
|
+
# @param type [PlayerType] Erster oder zweiter
|
15
16
|
# @param name [String] Name
|
16
|
-
def initialize(
|
17
|
-
@
|
17
|
+
def initialize(type, name)
|
18
|
+
@type = type
|
18
19
|
@name = name
|
19
20
|
end
|
20
21
|
|
@@ -3,7 +3,6 @@
|
|
3
3
|
require 'socket'
|
4
4
|
require_relative 'board'
|
5
5
|
require_relative 'set_move'
|
6
|
-
require_relative 'drag_move'
|
7
6
|
require_relative 'skip_move'
|
8
7
|
require_relative 'player'
|
9
8
|
require_relative 'network'
|
@@ -41,7 +40,7 @@ class Protocol
|
|
41
40
|
#
|
42
41
|
# @param text [String] the xml-string that will be parsed
|
43
42
|
def process_string(text)
|
44
|
-
logger.debug "Parse XML:\n#{text}\n----END XML"
|
43
|
+
#logger.debug "Parse XML:\n#{text}\n----END XML"
|
45
44
|
begin
|
46
45
|
REXML::Document.parse_stream(text, self)
|
47
46
|
rescue REXML::ParseException => e
|
@@ -50,7 +49,6 @@ class Protocol
|
|
50
49
|
end
|
51
50
|
end
|
52
51
|
|
53
|
-
|
54
52
|
# called when text is encountered
|
55
53
|
def text(text)
|
56
54
|
@context[:last_text] = text
|
@@ -63,6 +61,27 @@ class Protocol
|
|
63
61
|
case name
|
64
62
|
when 'board'
|
65
63
|
logger.debug @gamestate.board.to_s
|
64
|
+
when 'color'
|
65
|
+
if @context[:color] == :valid_colors
|
66
|
+
@gamestate.valid_colors << Color.to_a.find {|s| s.key == @context[:last_text].to_sym }
|
67
|
+
end
|
68
|
+
when 'shape'
|
69
|
+
case @context[:piece_target]
|
70
|
+
when :blue_shapes
|
71
|
+
last = @context[:last_text]
|
72
|
+
arr = PieceShape.to_a
|
73
|
+
shape = arr.find {|s| s.key == @context[:last_text].to_sym }
|
74
|
+
@gamestate.undeployed_blue_pieces << shape
|
75
|
+
when :yellow_shapes
|
76
|
+
shape = PieceShape.to_a.find {|s| s.key == @context[:last_text].to_sym }
|
77
|
+
@gamestate.undeployed_yellow_pieces << shape
|
78
|
+
when :red_shapes
|
79
|
+
shape = PieceShape.to_a.find {|s| s.key == @context[:last_text].to_sym }
|
80
|
+
@gamestate.undeployed_red_pieces << shape
|
81
|
+
when :green_shapes
|
82
|
+
shape = PieceShape.to_a.find {|s| s.key == @context[:last_text].to_sym }
|
83
|
+
@gamestate.undeployed_green_pieces << shape
|
84
|
+
end
|
66
85
|
end
|
67
86
|
end
|
68
87
|
|
@@ -98,58 +117,67 @@ class Protocol
|
|
98
117
|
logger.debug 'new gamestate'
|
99
118
|
@gamestate = GameState.new
|
100
119
|
@gamestate.turn = attrs['turn'].to_i
|
101
|
-
@gamestate.
|
102
|
-
@gamestate.
|
103
|
-
logger.debug "Turn: #{@gamestate.turn}"
|
104
|
-
when '
|
105
|
-
logger.debug 'new
|
106
|
-
player =
|
107
|
-
if player.color != PlayerColor::RED
|
108
|
-
throw new IllegalArgumentException("expected #{PlayerColor::RED} Player but got #{player.color}")
|
109
|
-
end
|
120
|
+
@gamestate.round = attrs['round'].to_i
|
121
|
+
@gamestate.start_piece = PieceShape.to_a.find {|s| s.key == attrs['startPiece'].to_sym }
|
122
|
+
logger.debug "Round: #{@gamestate.round}, Turn: #{@gamestate.turn}"
|
123
|
+
when 'first'
|
124
|
+
logger.debug 'new first player'
|
125
|
+
player = Player.new(PlayerType::ONE, attrs['displayName'])
|
110
126
|
@gamestate.add_player(player)
|
111
127
|
@context[:player] = player
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
throw new IllegalArgumentException("expected #{PlayerColor::BLUE} Player but got #{player.color}")
|
117
|
-
end
|
128
|
+
@context[:color] = :one
|
129
|
+
when 'second'
|
130
|
+
logger.debug 'new second player'
|
131
|
+
player = Player.new(PlayerType::TWO, attrs['displayName'])
|
118
132
|
@gamestate.add_player(player)
|
119
133
|
@context[:player] = player
|
134
|
+
@context[:color] = :two
|
135
|
+
when 'validColors'
|
136
|
+
@context[:color] = :valid_colors
|
137
|
+
@gamestate.valid_colors = []
|
120
138
|
when 'board'
|
121
139
|
logger.debug 'new board'
|
122
|
-
@gamestate.board = Board.new
|
140
|
+
@gamestate.board = Board.new()
|
123
141
|
when 'field'
|
124
142
|
x = attrs['x'].to_i
|
125
143
|
y = attrs['y'].to_i
|
126
|
-
|
127
|
-
field = Field.new(x, y,
|
144
|
+
color = Color.find_by_key(attrs['content'].to_sym)
|
145
|
+
field = Field.new(x, y, color)
|
128
146
|
@gamestate.board.add_field(field)
|
129
147
|
@context[:piece_target] = :field
|
130
148
|
@context[:field] = field
|
149
|
+
when 'blueShapes'
|
150
|
+
@context[:piece_target] = :blue_shapes
|
151
|
+
@gamestate.undeployed_blue_pieces = []
|
152
|
+
when 'yellowShapes'
|
153
|
+
@context[:piece_target] = :yellow_shapes
|
154
|
+
@gamestate.undeployed_yellow_pieces = []
|
155
|
+
when 'redShapes'
|
156
|
+
@context[:piece_target] = :red_shapes
|
157
|
+
@gamestate.undeployed_red_pieces = []
|
158
|
+
when 'greenShapes'
|
159
|
+
@context[:piece_target] = :green_shapes
|
160
|
+
@gamestate.undeployed_green_pieces = []
|
131
161
|
when 'piece'
|
132
|
-
|
133
|
-
|
134
|
-
|
162
|
+
color = Color.find_by_key(attrs['color'].to_sym)
|
163
|
+
kind = PieceShape.find_by_key(attrs['kind'].to_sym)
|
164
|
+
rotation = Rotation.find_by_key(attrs['rotation'].to_sym)
|
165
|
+
is_flipped = attrs['isFlipped'].downcase == "true"
|
166
|
+
piece = Piece.new(color, kind, rotation, is_flipped, Coordinates.origin)
|
135
167
|
case @context[:piece_target]
|
136
|
-
when :
|
137
|
-
@context[:field].add_piece(piece)
|
138
|
-
when :undeployed_red_pieces
|
139
|
-
@gamestate.undeployed_red_pieces << piece
|
140
|
-
when :undeployed_blue_pieces
|
168
|
+
when :blue_shapes
|
141
169
|
@gamestate.undeployed_blue_pieces << piece
|
170
|
+
when :yellow_shapes
|
171
|
+
@gamestate.undeployed_yellow_pieces << piece
|
172
|
+
when :red_shapes
|
173
|
+
@gamestate.green_red_pieces << piece
|
174
|
+
when :green_shapes
|
175
|
+
@gamestate.undeployed_green_pieces << piece
|
142
176
|
when :last_move
|
143
177
|
@context[:last_move_piece] = piece
|
144
178
|
else
|
145
179
|
raise "unknown piece target #{@context[:piece_target]}"
|
146
180
|
end
|
147
|
-
when 'undeployedRedPieces'
|
148
|
-
@context[:piece_target] = :undeployed_red_pieces
|
149
|
-
@gamestate.undeployed_red_pieces = []
|
150
|
-
when 'undeployedBluePieces'
|
151
|
-
@context[:piece_target] = :undeployed_blue_pieces
|
152
|
-
@gamestate.undeployed_blue_pieces = []
|
153
181
|
when 'lastMove'
|
154
182
|
type = attrs['class']
|
155
183
|
if type == 'skipmove'
|
@@ -158,24 +186,24 @@ class Protocol
|
|
158
186
|
@context[:last_move_type] = type
|
159
187
|
@context[:piece_target] = :last_move
|
160
188
|
end
|
161
|
-
when '
|
162
|
-
@context[:
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
@gamestate.last_move = SetMove.new(
|
168
|
-
when 'dragmove'
|
169
|
-
@gamestate.last_move = SetMove.new(@context[:last_move_start], destination)
|
189
|
+
when 'position'
|
190
|
+
case @context[:piece_target]
|
191
|
+
when :last_move
|
192
|
+
x = attrs['x'].to_i
|
193
|
+
y = attrs['y'].to_i
|
194
|
+
piece = @context[:last_move_piece]
|
195
|
+
@gamestate.last_move = SetMove.new(Piece.new(piece.color, piece.kind, piece.rotation, piece.is_flipped, Coordinates.new(x, y)))
|
170
196
|
end
|
197
|
+
when 'startColor'
|
198
|
+
@gamestate.start_color = Color::BLUE
|
171
199
|
when 'winner'
|
172
|
-
|
173
|
-
#winning_player = parsePlayer(attrs)
|
174
|
-
|
200
|
+
# TODO
|
201
|
+
# winning_player = parsePlayer(attrs)
|
202
|
+
# @gamestate.condition = Condition.new(winning_player, @gamestate.condition.reason)
|
175
203
|
when 'score'
|
176
204
|
# TODO
|
177
205
|
# there are two score tags in the result, but reason attribute should be equal on both
|
178
|
-
|
206
|
+
# @gamestate.condition = Condition.new(@gamestate.condition.winner, attrs['reason'])
|
179
207
|
when 'left'
|
180
208
|
logger.debug 'got left event, terminating'
|
181
209
|
@network.disconnect
|
@@ -185,17 +213,6 @@ class Protocol
|
|
185
213
|
end
|
186
214
|
end
|
187
215
|
|
188
|
-
# Converts XML attributes for a Player to a new Player object
|
189
|
-
#
|
190
|
-
# @param attributes [Hash] Attributes for the new Player.
|
191
|
-
# @return [Player] The created Player object.
|
192
|
-
def parsePlayer(attributes)
|
193
|
-
Player.new(
|
194
|
-
PlayerColor.find_by_key(attributes['color'].to_sym),
|
195
|
-
attributes['displayName']
|
196
|
-
)
|
197
|
-
end
|
198
|
-
|
199
216
|
# send a xml document
|
200
217
|
#
|
201
218
|
# @param document [REXML::Document] the document, that will be send to the connected server
|
@@ -228,26 +245,17 @@ class Protocol
|
|
228
245
|
# structures.
|
229
246
|
case move
|
230
247
|
when SetMove
|
231
|
-
builder.data(class: '
|
232
|
-
data.piece(
|
233
|
-
|
234
|
-
data.destination(x: d.x, y: d.y, z: d.z)
|
235
|
-
move.hints.each do |hint|
|
236
|
-
data.hint(content: hint.content)
|
248
|
+
builder.data(class: 'sc.plugin2021.SetMove') do |data|
|
249
|
+
data.piece(color: move.piece.color, kind: move.piece.kind, rotation: move.piece.rotation, isFlipped: move.piece.is_flipped) do |piece|
|
250
|
+
piece.position(x: move.piece.position.x, y: move.piece.position.y)
|
237
251
|
end
|
238
|
-
end
|
239
|
-
when DragMove
|
240
|
-
builder.data(class: 'dragmove') do |data|
|
241
|
-
s = move.start
|
242
|
-
data.start(x: s.x, y: s.y, z: s.z)
|
243
|
-
d = move.destination
|
244
|
-
data.destination(x: d.x, y: d.y, z: d.z)
|
245
252
|
move.hints.each do |hint|
|
246
253
|
data.hint(content: hint.content)
|
247
254
|
end
|
248
255
|
end
|
249
256
|
when SkipMove
|
250
|
-
builder.data(class: '
|
257
|
+
builder.data(class: 'sc.plugin2021.SkipMove') do |data|
|
258
|
+
data.color(@gamestate.current_color.key.to_s)
|
251
259
|
move.hints.each do |hint|
|
252
260
|
data.hint(content: hint.content)
|
253
261
|
end
|
@@ -255,5 +263,4 @@ class Protocol
|
|
255
263
|
end
|
256
264
|
builder.target!
|
257
265
|
end
|
258
|
-
|
259
266
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'typesafe_enum'
|
4
|
+
|
5
|
+
# Die Drehung eines Steins
|
6
|
+
class Rotation < TypesafeEnum::Base
|
7
|
+
new :NONE, 0
|
8
|
+
new :RIGHT, 1
|
9
|
+
new :MIRROR, 2
|
10
|
+
new :LEFT, 3
|
11
|
+
|
12
|
+
# Summiere beide Rotationen auf.
|
13
|
+
# (Die resultierende Rotation hat den gleichen Effekt wie die beiden Rotationen einzeln).
|
14
|
+
def rotate(rotation)
|
15
|
+
Rotation.to_a[(value + rotation.value) % Rotation.size]
|
16
|
+
end
|
17
|
+
|
18
|
+
# Gibt den rotation namen zurück
|
19
|
+
def to_s
|
20
|
+
self.key.to_s
|
21
|
+
end
|
22
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
2
3
|
require_relative 'board'
|
3
4
|
require_relative 'client_interface'
|
4
5
|
require_relative 'network'
|
@@ -8,7 +9,7 @@ class Runner
|
|
8
9
|
include Logging
|
9
10
|
|
10
11
|
def initialize(host, port, client, reservation = nil)
|
11
|
-
logger.info 'Software Challenge
|
12
|
+
logger.info 'Software Challenge 2021'
|
12
13
|
logger.info 'Ruby Client'
|
13
14
|
logger.info "Host: #{host}"
|
14
15
|
logger.info "Port: #{port}"
|
@@ -1,15 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'has_hints'
|
2
4
|
|
5
|
+
# Ein SetMove platziert einen Stein auf dem Spielbrett
|
3
6
|
class SetMove
|
4
|
-
|
5
7
|
include HasHints
|
6
8
|
|
7
9
|
attr_reader :piece
|
8
|
-
attr_reader :destination
|
9
10
|
|
10
|
-
|
11
|
+
# Erstellt ein neuen leeren Legezug.
|
12
|
+
def initialize(piece)
|
11
13
|
@piece = piece
|
12
|
-
@destination = destination
|
13
14
|
@hints = []
|
14
15
|
end
|
16
|
+
|
17
|
+
def ==(other)
|
18
|
+
piece == other.piece
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
"SetMove(#{piece}"
|
23
|
+
end
|
15
24
|
end
|
@@ -1,7 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'has_hints'
|
4
|
+
|
5
|
+
# Ein SkipMove ziegt an, dass die aktuelle Farbe keinen Stein platzieren will
|
2
6
|
class SkipMove
|
3
7
|
include HasHints
|
4
8
|
|
9
|
+
# Erstellt ein neuen leeren Aussetzzug.
|
5
10
|
def initialize
|
6
11
|
@hints = []
|
7
12
|
end
|
@@ -4,8 +4,7 @@
|
|
4
4
|
# Konstanten zum aktuellen Spiel.
|
5
5
|
module Constants
|
6
6
|
ROUND_LIMIT = 30 # Rundenbegrenzung. Nach Ende der angegebenen Runde endet auch das Spiel.
|
7
|
-
GAME_IDENTIFIER = '
|
8
|
-
|
9
|
-
|
10
|
-
SHIFT = ((BOARD_SIZE - 1) / 2) # Wert, den man auf eine CubeCoordinate addieren muss, um einen positiven Index (fuer das Feld-Array) zu bekommen
|
7
|
+
GAME_IDENTIFIER = 'swc_2021_blokus' # Der Identifikator des Spiels. Für die Kommunikation mit dem Spielserver.
|
8
|
+
BOARD_SIZE = 20 # Seitenlänge des Spielbretts in Feldern
|
9
|
+
TOTAL_PIECE_SHAPES = 21
|
11
10
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
# This creates a entrypoint for the gem named 'software_challenge_client.rb'
|
3
|
+
# which includes all ruby files under lib. Making sure that everthing is
|
4
|
+
# included after some files were added or removed (in the process of updating
|
5
|
+
# the gem for a new game).
|
6
|
+
shopt -s globstar || exit 1
|
7
|
+
cd lib || exit 1
|
8
|
+
FILENAME='software_challenge_client.rb'
|
9
|
+
|
10
|
+
echo "# frozen_string_literal: true
|
11
|
+
module SoftwareChallengeClient" > $FILENAME
|
12
|
+
for file in software_challenge_client/**/*.rb; do
|
13
|
+
echo " require '${file}'"
|
14
|
+
done >> $FILENAME;
|
15
|
+
echo "end" >> $FILENAME
|
@@ -1,39 +1,41 @@
|
|
1
1
|
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
2
3
|
lib = File.expand_path('../lib', __FILE__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require 'software_challenge_client/version'
|
5
6
|
|
6
7
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
8
|
+
spec.name = 'software_challenge_client'
|
8
9
|
spec.version = SoftwareChallengeClient::VERSION
|
9
10
|
spec.authors = File.readlines('AUTHORS').select { |l| l[' <'] }.map { |l| l.match(/^(.*) *</)[1] }
|
10
11
|
spec.email = File.readlines('AUTHORS').select { |l| l[' <'] }.map { |l| l.match(/<(.*)>/)[1] }
|
11
12
|
|
12
|
-
spec.summary = '
|
13
|
+
spec.summary = 'Provides functions to build a client for the coding competition Software-Challenge Germany.'
|
13
14
|
spec.description = ''
|
14
15
|
spec.homepage = 'http://www.software-challenge.de'
|
15
16
|
|
16
17
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
-
spec.bindir =
|
18
|
+
spec.bindir = 'exe'
|
18
19
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
-
spec.require_paths = [
|
20
|
+
spec.require_paths = ['lib']
|
20
21
|
|
21
|
-
spec.required_ruby_version = '>= 2.
|
22
|
-
spec.add_dependency 'typesafe_enum'
|
22
|
+
spec.required_ruby_version = '>= 2.5.5'
|
23
23
|
spec.add_dependency 'builder'
|
24
|
+
spec.add_dependency 'typesafe_enum'
|
24
25
|
|
25
26
|
spec.add_development_dependency 'bundler', '>= 1.10'
|
26
|
-
spec.add_development_dependency 'rake', '>= 10.0'
|
27
|
-
spec.add_development_dependency 'yard', '>= 0.8'
|
28
|
-
spec.add_development_dependency 'rspec'
|
29
27
|
spec.add_development_dependency 'fuubar'
|
30
|
-
spec.add_development_dependency 'rubocop'
|
31
|
-
spec.add_development_dependency 'rubocop-rspec'
|
32
28
|
spec.add_development_dependency 'guard'
|
33
29
|
spec.add_development_dependency 'guard-rspec'
|
34
30
|
spec.add_development_dependency 'guard-rubocop'
|
35
31
|
spec.add_development_dependency 'pry'
|
36
|
-
spec.add_development_dependency 'pry-rescue'
|
37
|
-
spec.add_development_dependency 'pry-coolline'
|
38
32
|
spec.add_development_dependency 'pry-byebug'
|
33
|
+
spec.add_development_dependency 'pry-coolline'
|
34
|
+
spec.add_development_dependency 'pry-rescue'
|
35
|
+
spec.add_development_dependency 'rake', '>= 10.0'
|
36
|
+
spec.add_development_dependency 'rspec'
|
37
|
+
spec.add_development_dependency 'rubocop'
|
38
|
+
spec.add_development_dependency 'rubocop-rspec'
|
39
|
+
spec.add_development_dependency 'solargraph'
|
40
|
+
spec.add_development_dependency 'yard', '>= 0.8'
|
39
41
|
end
|