software_challenge_client 20.2.4 → 21.2.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 +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
|