sashite-pcn 0.4.0 → 0.5.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/README.md +175 -6
- data/lib/sashite/pcn/game/sides.rb +1 -1
- data/lib/sashite/pcn/game.rb +127 -3
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1e856d0d80b2560d24cfd77f6359b655d4b5d67f52ad320e04504f79cab49536
|
|
4
|
+
data.tar.gz: e539e626bd827a49071f2c80cc9095aa13abf4965433e8f14245b4461308009a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: abdfa7ddc44437089a0a8a3d7e2a959692f33f00760a273b1df37a86d8c125fb16ef684da67a7bf382f0a67b598e5083cbb569a6de967b40b9527dd9b6d47c4d
|
|
7
|
+
data.tar.gz: ce3dc76261263878b55a13995784074c5c1c3b4ef04a213c47b406e8faad7406a745bb232bab09a72a86cb22c8a5fdbebc10f09e8f697d3622e60451fffa1f6c
|
data/README.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
- [API Documentation](#api-documentation)
|
|
16
16
|
- [Format Specifications](#format-specifications)
|
|
17
17
|
- [Time Control Examples](#time-control-examples)
|
|
18
|
+
- [Draw Offers](#draw-offers)
|
|
18
19
|
- [Error Handling](#error-handling)
|
|
19
20
|
- [Complete Examples](#complete-examples)
|
|
20
21
|
- [JSON Interoperability](#json-interoperability)
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
PCN (Portable Chess Notation) is a comprehensive, JSON-based format for representing complete chess game records across variants. This Ruby implementation provides:
|
|
25
26
|
|
|
26
27
|
- **Complete game records** with positions, moves, time tracking, and metadata
|
|
28
|
+
- **Draw offer tracking** for recording draw proposals between players
|
|
27
29
|
- **Time control support** for Fischer, Classical, Byōyomi, Canadian, and more
|
|
28
30
|
- **Rule-agnostic design** supporting all abstract strategy board games
|
|
29
31
|
- **Immutable objects** with functional transformations
|
|
@@ -80,11 +82,16 @@ game.status # => CGSN status object
|
|
|
80
82
|
# Transform immutably
|
|
81
83
|
new_game = game.add_move(["g1-f3", 1.8])
|
|
82
84
|
final_game = new_game.with_status("checkmate")
|
|
85
|
+
|
|
86
|
+
# Handle draw offers
|
|
87
|
+
game_with_offer = game.with_draw_offered_by("first")
|
|
88
|
+
game.draw_offered? # => true
|
|
89
|
+
game.draw_offered_by # => "first"
|
|
83
90
|
```
|
|
84
91
|
|
|
85
92
|
## API Documentation
|
|
86
93
|
|
|
87
|
-
For complete API documentation, see [API
|
|
94
|
+
For complete API documentation, see [API Reference](https://rubydoc.info/github/sashite/pcn.rb/main/file/API.md).
|
|
88
95
|
|
|
89
96
|
The API documentation includes:
|
|
90
97
|
- All classes and methods
|
|
@@ -93,6 +100,7 @@ The API documentation includes:
|
|
|
93
100
|
- Code examples for every method
|
|
94
101
|
- Common usage patterns
|
|
95
102
|
- Time control formats
|
|
103
|
+
- Draw offer handling
|
|
96
104
|
- Error handling
|
|
97
105
|
|
|
98
106
|
## Format Specifications
|
|
@@ -102,7 +110,7 @@ The API documentation includes:
|
|
|
102
110
|
```ruby
|
|
103
111
|
# Standard chess starting position
|
|
104
112
|
"+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c"
|
|
105
|
-
# └─ board
|
|
113
|
+
# └─ board ──────────────────────────────────────────────┘ └┘ └─┘
|
|
106
114
|
# turn styles
|
|
107
115
|
|
|
108
116
|
# Empty board
|
|
@@ -150,8 +158,10 @@ The API documentation includes:
|
|
|
150
158
|
# Explicit only (must be declared)
|
|
151
159
|
"resignation" # Player resigned
|
|
152
160
|
"time_limit" # Time expired
|
|
153
|
-
"agreement" # Mutual agreement
|
|
161
|
+
"agreement" # Mutual agreement (draw)
|
|
154
162
|
"illegal_move" # Invalid move played
|
|
163
|
+
"repetition" # Draw by repetition
|
|
164
|
+
"move_limit" # Move limit reached
|
|
155
165
|
```
|
|
156
166
|
|
|
157
167
|
### SNN (Styles)
|
|
@@ -232,6 +242,74 @@ periods: [] # Empty array
|
|
|
232
242
|
periods: nil # Or omit entirely
|
|
233
243
|
```
|
|
234
244
|
|
|
245
|
+
## Draw Offers
|
|
246
|
+
|
|
247
|
+
PCN supports tracking draw offers between players using the `draw_offered_by` field.
|
|
248
|
+
|
|
249
|
+
### Basic Usage
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
# Offer a draw
|
|
253
|
+
game = game.with_draw_offered_by("first") # First player offers
|
|
254
|
+
|
|
255
|
+
# Check if draw offered
|
|
256
|
+
game.draw_offered? # => true
|
|
257
|
+
game.draw_offered_by # => "first"
|
|
258
|
+
|
|
259
|
+
# Accept the draw
|
|
260
|
+
game = game.with_status("agreement")
|
|
261
|
+
|
|
262
|
+
# Decline/withdraw draw offer
|
|
263
|
+
game = game.with_draw_offered_by(nil)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Draw Offer Values
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
nil # No draw offer pending (default)
|
|
270
|
+
"first" # First player has offered a draw
|
|
271
|
+
"second" # Second player has offered a draw
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Example: Draw Offer During Game
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
# Game in progress with draw offer
|
|
278
|
+
game = Sashite::Pcn.parse({
|
|
279
|
+
"setup" => "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c",
|
|
280
|
+
"moves" => [
|
|
281
|
+
["e2-e4", 8.0],
|
|
282
|
+
["e7-e5", 12.0],
|
|
283
|
+
["g1-f3", 15.0]
|
|
284
|
+
],
|
|
285
|
+
"draw_offered_by" => "first",
|
|
286
|
+
"status" => "in_progress"
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
# First player has offered a draw after move 3
|
|
290
|
+
puts "Draw offered by: #{game.draw_offered_by}" # => "first"
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Example: Accepted Draw
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
# Draw accepted
|
|
297
|
+
game = Sashite::Pcn.parse({
|
|
298
|
+
"setup" => "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c",
|
|
299
|
+
"moves" => [
|
|
300
|
+
["e2-e4", 15.0],
|
|
301
|
+
["e7-e5", 18.0],
|
|
302
|
+
["g1-f3", 22.0],
|
|
303
|
+
["b8-c6", 12.0]
|
|
304
|
+
],
|
|
305
|
+
"draw_offered_by" => "first",
|
|
306
|
+
"status" => "agreement"
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
# First player offered, second player accepted
|
|
310
|
+
puts "Game result: Draw by agreement"
|
|
311
|
+
```
|
|
312
|
+
|
|
235
313
|
## Error Handling
|
|
236
314
|
|
|
237
315
|
```ruby
|
|
@@ -256,6 +334,16 @@ rescue ArgumentError => e
|
|
|
256
334
|
puts e.message # => "Each move must be [PAN string, seconds float] tuple"
|
|
257
335
|
end
|
|
258
336
|
|
|
337
|
+
# Draw offer validation
|
|
338
|
+
begin
|
|
339
|
+
Sashite::Pcn::Game.new(
|
|
340
|
+
setup: "8/8/8/8/8/8/8/8 / U/u",
|
|
341
|
+
draw_offered_by: "third" # Invalid: must be nil, "first", or "second"
|
|
342
|
+
)
|
|
343
|
+
rescue ArgumentError => e
|
|
344
|
+
puts e.message # => "draw_offered_by must be nil, 'first', or 'second'"
|
|
345
|
+
end
|
|
346
|
+
|
|
259
347
|
# Metadata validation
|
|
260
348
|
begin
|
|
261
349
|
Sashite::Pcn::Game.new(
|
|
@@ -275,6 +363,7 @@ begin
|
|
|
275
363
|
]
|
|
276
364
|
}
|
|
277
365
|
}
|
|
366
|
+
Sashite::Pcn::Game.new(setup: "8/8/8/8/8/8/8/8 / U/u", sides: sides)
|
|
278
367
|
rescue ArgumentError => e
|
|
279
368
|
puts e.message # => "time must be a non-negative integer (>= 0)"
|
|
280
369
|
end
|
|
@@ -353,14 +442,58 @@ puts "Moves played: #{game.move_count}"
|
|
|
353
442
|
puts "White time: #{game.first_player_time}s"
|
|
354
443
|
puts "Black time: #{game.second_player_time}s"
|
|
355
444
|
|
|
445
|
+
# Offer a draw
|
|
446
|
+
game = game.with_draw_offered_by("first")
|
|
447
|
+
|
|
356
448
|
# Finish game
|
|
357
449
|
if some_condition
|
|
358
450
|
game = game.with_status("checkmate")
|
|
359
|
-
elsif
|
|
451
|
+
elsif draw_accepted?
|
|
452
|
+
game = game.with_status("agreement")
|
|
453
|
+
else
|
|
360
454
|
game = game.with_status("resignation")
|
|
361
455
|
end
|
|
362
456
|
```
|
|
363
457
|
|
|
458
|
+
### Game with Draw Offer
|
|
459
|
+
|
|
460
|
+
```ruby
|
|
461
|
+
# Complete game with draw offer and acceptance
|
|
462
|
+
game = Sashite::Pcn::Game.new(
|
|
463
|
+
meta: {
|
|
464
|
+
event: "Club Match",
|
|
465
|
+
round: 5,
|
|
466
|
+
started_at: "2025-01-27T14:00:00Z"
|
|
467
|
+
},
|
|
468
|
+
sides: {
|
|
469
|
+
first: {
|
|
470
|
+
name: "Player A",
|
|
471
|
+
elo: 2200,
|
|
472
|
+
style: "CHESS"
|
|
473
|
+
},
|
|
474
|
+
second: {
|
|
475
|
+
name: "Player B",
|
|
476
|
+
elo: 2190,
|
|
477
|
+
style: "chess"
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
setup: "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c",
|
|
481
|
+
moves: [
|
|
482
|
+
["e2-e4", 15.0],
|
|
483
|
+
["e7-e5", 18.0],
|
|
484
|
+
["g1-f3", 22.0],
|
|
485
|
+
["b8-c6", 12.0],
|
|
486
|
+
["d2-d4", 31.0],
|
|
487
|
+
["e5+d4", 25.0]
|
|
488
|
+
],
|
|
489
|
+
draw_offered_by: "first",
|
|
490
|
+
status: "agreement"
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
puts "Result: Draw by agreement"
|
|
494
|
+
puts "Initiated by: #{game.draw_offered_by}"
|
|
495
|
+
```
|
|
496
|
+
|
|
364
497
|
### Complex Tournament Game
|
|
365
498
|
|
|
366
499
|
```ruby
|
|
@@ -437,7 +570,41 @@ puts "Winner: #{game.status == 'resignation' ? 'First player (White)' : 'Unknown
|
|
|
437
570
|
puts "Total moves: #{game.move_count}"
|
|
438
571
|
|
|
439
572
|
# Export to JSON file
|
|
440
|
-
File.write("game.json", JSON.
|
|
573
|
+
File.write("game.json", JSON.pretty_generate(game.to_h))
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Draw Offer Scenario
|
|
577
|
+
|
|
578
|
+
```ruby
|
|
579
|
+
# Game progressing with draw offer
|
|
580
|
+
game = Sashite::Pcn::Game.new(
|
|
581
|
+
setup: "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c"
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
# Play several moves
|
|
585
|
+
game = game.add_move(["e2-e4", 8.0])
|
|
586
|
+
game = game.add_move(["e7-e5", 12.0])
|
|
587
|
+
game = game.add_move(["g1-f3", 15.0])
|
|
588
|
+
game = game.add_move(["b8-c6", 5.0])
|
|
589
|
+
|
|
590
|
+
# First player offers a draw
|
|
591
|
+
game = game.with_draw_offered_by("first")
|
|
592
|
+
|
|
593
|
+
# Check the offer
|
|
594
|
+
if game.draw_offered?
|
|
595
|
+
puts "Draw offered by: #{game.draw_offered_by}"
|
|
596
|
+
|
|
597
|
+
# Second player can accept
|
|
598
|
+
if player_accepts_draw?
|
|
599
|
+
game = game.with_status("agreement")
|
|
600
|
+
puts "Draw accepted!"
|
|
601
|
+
else
|
|
602
|
+
# Or decline and continue
|
|
603
|
+
game = game.with_draw_offered_by(nil)
|
|
604
|
+
game = game.add_move(["f1-c4", 9.0])
|
|
605
|
+
puts "Draw declined, game continues"
|
|
606
|
+
end
|
|
607
|
+
end
|
|
441
608
|
```
|
|
442
609
|
|
|
443
610
|
## JSON Interoperability
|
|
@@ -511,6 +678,7 @@ record.save!
|
|
|
511
678
|
record = GameRecord.find(id)
|
|
512
679
|
game = record.game
|
|
513
680
|
puts game.move_count
|
|
681
|
+
puts "Draw offered: #{game.draw_offered?}"
|
|
514
682
|
```
|
|
515
683
|
|
|
516
684
|
## Properties
|
|
@@ -520,13 +688,14 @@ puts game.move_count
|
|
|
520
688
|
- **Type-safe**: Strong type checking throughout
|
|
521
689
|
- **Rule-agnostic**: Independent of specific game rules
|
|
522
690
|
- **JSON-native**: Direct serialization to/from JSON
|
|
523
|
-
- **Comprehensive**: Complete game information including time tracking
|
|
691
|
+
- **Comprehensive**: Complete game information including time tracking and draw offers
|
|
524
692
|
- **Extensible**: Custom metadata and player fields supported
|
|
525
693
|
|
|
526
694
|
## Documentation
|
|
527
695
|
|
|
528
696
|
- [Official PCN Specification v1.0.0](https://sashite.dev/specs/pcn/1.0.0/)
|
|
529
697
|
- [PCN Examples](https://sashite.dev/specs/pcn/1.0.0/examples/)
|
|
698
|
+
- [Draw Offer Examples](https://sashite.dev/specs/pcn/1.0.0/examples/draw-offers/)
|
|
530
699
|
- [API Documentation](https://rubydoc.info/github/sashite/pcn.rb/main)
|
|
531
700
|
- [PAN Specification](https://sashite.dev/specs/pan/) (moves)
|
|
532
701
|
- [FEEN Specification](https://sashite.dev/specs/feen/) (positions)
|
|
@@ -404,7 +404,7 @@ module Sashite
|
|
|
404
404
|
|
|
405
405
|
# Describe time control periods in human-readable format
|
|
406
406
|
#
|
|
407
|
-
# @param periods [Array<Hash
|
|
407
|
+
# @param periods [Array<Hash>] period array
|
|
408
408
|
# @return [String] description
|
|
409
409
|
def describe_periods(periods)
|
|
410
410
|
return "unlimited" if periods.empty?
|
data/lib/sashite/pcn/game.rb
CHANGED
|
@@ -13,8 +13,8 @@ module Sashite
|
|
|
13
13
|
# Represents a complete game record in PCN (Portable Chess Notation) format.
|
|
14
14
|
#
|
|
15
15
|
# A game consists of an initial position (setup), optional move sequence with time tracking,
|
|
16
|
-
# optional game status, optional metadata, and optional player
|
|
17
|
-
# All instances are immutable - transformations return new instances.
|
|
16
|
+
# optional game status, optional draw offer tracking, optional metadata, and optional player
|
|
17
|
+
# information with time control. All instances are immutable - transformations return new instances.
|
|
18
18
|
#
|
|
19
19
|
# All parameters are validated at initialization time. An instance of Game
|
|
20
20
|
# cannot be created with invalid data.
|
|
@@ -55,6 +55,14 @@ module Sashite
|
|
|
55
55
|
# ],
|
|
56
56
|
# status: "in_progress"
|
|
57
57
|
# )
|
|
58
|
+
#
|
|
59
|
+
# @example Game with draw offer
|
|
60
|
+
# game = Game.new(
|
|
61
|
+
# setup: "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c",
|
|
62
|
+
# moves: [["e2-e4", 8.0], ["e7-e5", 12.0]],
|
|
63
|
+
# draw_offered_by: "first",
|
|
64
|
+
# status: "in_progress"
|
|
65
|
+
# )
|
|
58
66
|
class Game
|
|
59
67
|
# Error messages
|
|
60
68
|
ERROR_MISSING_SETUP = "setup is required"
|
|
@@ -64,19 +72,24 @@ module Sashite
|
|
|
64
72
|
ERROR_INVALID_SECONDS = "seconds must be a non-negative number"
|
|
65
73
|
ERROR_INVALID_META = "meta must be a hash"
|
|
66
74
|
ERROR_INVALID_SIDES = "sides must be a hash"
|
|
75
|
+
ERROR_INVALID_DRAW_OFFERED_BY = "draw_offered_by must be nil, 'first', or 'second'"
|
|
67
76
|
|
|
68
77
|
# Status constants
|
|
69
78
|
STATUS_IN_PROGRESS = "in_progress"
|
|
70
79
|
|
|
80
|
+
# Valid draw_offered_by values
|
|
81
|
+
VALID_DRAW_OFFERED_BY = [nil, "first", "second"].freeze
|
|
82
|
+
|
|
71
83
|
# Create a new game instance
|
|
72
84
|
#
|
|
73
85
|
# @param setup [String] initial position in FEEN format (required)
|
|
74
86
|
# @param moves [Array<Array>] sequence of [PAN, seconds] tuples (optional, defaults to [])
|
|
75
87
|
# @param status [String, nil] game status in CGSN format (optional)
|
|
88
|
+
# @param draw_offered_by [String, nil] draw offer indicator: nil, "first", or "second" (optional)
|
|
76
89
|
# @param meta [Hash] game metadata (optional)
|
|
77
90
|
# @param sides [Hash] player information with time control (optional)
|
|
78
91
|
# @raise [ArgumentError] if required fields are missing or invalid
|
|
79
|
-
def initialize(setup:, moves: [], status: nil, meta: {}, sides: {})
|
|
92
|
+
def initialize(setup:, moves: [], status: nil, draw_offered_by: nil, meta: {}, sides: {})
|
|
80
93
|
# Validate and parse setup (required)
|
|
81
94
|
raise ::ArgumentError, ERROR_MISSING_SETUP if setup.nil?
|
|
82
95
|
@setup = ::Sashite::Feen.parse(setup)
|
|
@@ -88,6 +101,10 @@ module Sashite
|
|
|
88
101
|
# Validate and parse status (optional)
|
|
89
102
|
@status = status.nil? ? nil : ::Sashite::Cgsn.parse(status)
|
|
90
103
|
|
|
104
|
+
# Validate draw_offered_by (optional)
|
|
105
|
+
validate_draw_offered_by(draw_offered_by)
|
|
106
|
+
@draw_offered_by = draw_offered_by
|
|
107
|
+
|
|
91
108
|
# Validate meta (optional)
|
|
92
109
|
raise ::ArgumentError, ERROR_INVALID_META unless meta.is_a?(::Hash)
|
|
93
110
|
@meta = Meta.new(**meta.transform_keys(&:to_sym))
|
|
@@ -153,6 +170,17 @@ module Sashite
|
|
|
153
170
|
@status
|
|
154
171
|
end
|
|
155
172
|
|
|
173
|
+
# Get draw offer indicator
|
|
174
|
+
#
|
|
175
|
+
# @return [String, nil] "first", "second", or nil
|
|
176
|
+
#
|
|
177
|
+
# @example
|
|
178
|
+
# game.draw_offered_by # => "first"
|
|
179
|
+
# game.draw_offered_by # => nil
|
|
180
|
+
def draw_offered_by
|
|
181
|
+
@draw_offered_by
|
|
182
|
+
end
|
|
183
|
+
|
|
156
184
|
# ========================================================================
|
|
157
185
|
# Player Access
|
|
158
186
|
# ========================================================================
|
|
@@ -221,6 +249,7 @@ module Sashite
|
|
|
221
249
|
setup: @setup.to_s,
|
|
222
250
|
moves: new_moves,
|
|
223
251
|
status: @status&.to_s,
|
|
252
|
+
draw_offered_by: @draw_offered_by,
|
|
224
253
|
meta: @meta.to_h,
|
|
225
254
|
sides: @sides.to_h
|
|
226
255
|
)
|
|
@@ -334,6 +363,30 @@ module Sashite
|
|
|
334
363
|
setup: @setup.to_s,
|
|
335
364
|
moves: @moves,
|
|
336
365
|
status: new_status,
|
|
366
|
+
draw_offered_by: @draw_offered_by,
|
|
367
|
+
meta: @meta.to_h,
|
|
368
|
+
sides: @sides.to_h
|
|
369
|
+
)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Create new game with updated draw offer
|
|
373
|
+
#
|
|
374
|
+
# @param player [String, nil] "first", "second", or nil
|
|
375
|
+
# @return [Game] new game instance with updated draw offer
|
|
376
|
+
# @raise [ArgumentError] if player is invalid
|
|
377
|
+
#
|
|
378
|
+
# @example
|
|
379
|
+
# # First player offers a draw
|
|
380
|
+
# game_with_offer = game.with_draw_offered_by("first")
|
|
381
|
+
#
|
|
382
|
+
# # Withdraw draw offer
|
|
383
|
+
# game_no_offer = game.with_draw_offered_by(nil)
|
|
384
|
+
def with_draw_offered_by(player)
|
|
385
|
+
self.class.new(
|
|
386
|
+
setup: @setup.to_s,
|
|
387
|
+
moves: @moves,
|
|
388
|
+
status: @status&.to_s,
|
|
389
|
+
draw_offered_by: player,
|
|
337
390
|
meta: @meta.to_h,
|
|
338
391
|
sides: @sides.to_h
|
|
339
392
|
)
|
|
@@ -352,6 +405,7 @@ module Sashite
|
|
|
352
405
|
setup: @setup.to_s,
|
|
353
406
|
moves: @moves,
|
|
354
407
|
status: @status&.to_s,
|
|
408
|
+
draw_offered_by: @draw_offered_by,
|
|
355
409
|
meta: merged_meta,
|
|
356
410
|
sides: @sides.to_h
|
|
357
411
|
)
|
|
@@ -370,6 +424,7 @@ module Sashite
|
|
|
370
424
|
setup: @setup.to_s,
|
|
371
425
|
moves: new_moves,
|
|
372
426
|
status: @status&.to_s,
|
|
427
|
+
draw_offered_by: @draw_offered_by,
|
|
373
428
|
meta: @meta.to_h,
|
|
374
429
|
sides: @sides.to_h
|
|
375
430
|
)
|
|
@@ -403,6 +458,17 @@ module Sashite
|
|
|
403
458
|
!in_progress?
|
|
404
459
|
end
|
|
405
460
|
|
|
461
|
+
# Check if a draw offer is pending
|
|
462
|
+
#
|
|
463
|
+
# @return [Boolean] true if a draw offer is pending
|
|
464
|
+
#
|
|
465
|
+
# @example
|
|
466
|
+
# game.draw_offered? # => true (if draw_offered_by is "first" or "second")
|
|
467
|
+
# game.draw_offered? # => false (if draw_offered_by is nil)
|
|
468
|
+
def draw_offered?
|
|
469
|
+
!@draw_offered_by.nil?
|
|
470
|
+
end
|
|
471
|
+
|
|
406
472
|
# ========================================================================
|
|
407
473
|
# Serialization
|
|
408
474
|
# ========================================================================
|
|
@@ -417,6 +483,7 @@ module Sashite
|
|
|
417
483
|
# # "setup" => "...",
|
|
418
484
|
# # "moves" => [["e2-e4", 2.5], ["e7-e5", 3.1]],
|
|
419
485
|
# # "status" => "in_progress",
|
|
486
|
+
# # "draw_offered_by" => "first",
|
|
420
487
|
# # "meta" => {...},
|
|
421
488
|
# # "sides" => {...}
|
|
422
489
|
# # }
|
|
@@ -428,12 +495,59 @@ module Sashite
|
|
|
428
495
|
|
|
429
496
|
# Include optional fields if present
|
|
430
497
|
result["status"] = @status.to_s if @status
|
|
498
|
+
result["draw_offered_by"] = @draw_offered_by if @draw_offered_by
|
|
431
499
|
result["meta"] = @meta.to_h unless @meta.empty?
|
|
432
500
|
result["sides"] = @sides.to_h unless @sides.empty?
|
|
433
501
|
|
|
434
502
|
result
|
|
435
503
|
end
|
|
436
504
|
|
|
505
|
+
# Compare with another game
|
|
506
|
+
#
|
|
507
|
+
# @param other [Object] object to compare
|
|
508
|
+
# @return [Boolean] true if equal
|
|
509
|
+
#
|
|
510
|
+
# @example
|
|
511
|
+
# game1 == game2 # => true if all attributes match
|
|
512
|
+
def ==(other)
|
|
513
|
+
return false unless other.is_a?(Game)
|
|
514
|
+
|
|
515
|
+
@setup.to_s == other.setup.to_s &&
|
|
516
|
+
@moves == other.moves &&
|
|
517
|
+
@status&.to_s == other.status&.to_s &&
|
|
518
|
+
@draw_offered_by == other.draw_offered_by &&
|
|
519
|
+
@meta == other.meta &&
|
|
520
|
+
@sides == other.sides
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# Generate hash code
|
|
524
|
+
#
|
|
525
|
+
# @return [Integer] hash code for this game
|
|
526
|
+
#
|
|
527
|
+
# @example
|
|
528
|
+
# game.hash # => 123456789
|
|
529
|
+
def hash
|
|
530
|
+
[@setup.to_s, @moves, @status&.to_s, @draw_offered_by, @meta, @sides].hash
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Generate debug representation
|
|
534
|
+
#
|
|
535
|
+
# @return [String] debug string
|
|
536
|
+
#
|
|
537
|
+
# @example
|
|
538
|
+
# game.inspect
|
|
539
|
+
# # => "#<Game setup=\"...\" moves=[...] status=\"in_progress\" draw_offered_by=\"first\">"
|
|
540
|
+
def inspect
|
|
541
|
+
parts = ["setup=#{@setup.to_s.inspect}"]
|
|
542
|
+
parts << "moves=#{@moves.inspect}"
|
|
543
|
+
parts << "status=#{@status&.to_s.inspect}" if @status
|
|
544
|
+
parts << "draw_offered_by=#{@draw_offered_by.inspect}" if @draw_offered_by
|
|
545
|
+
parts << "meta=#{@meta.inspect}" unless @meta.empty?
|
|
546
|
+
parts << "sides=#{@sides.inspect}" unless @sides.empty?
|
|
547
|
+
|
|
548
|
+
"#<#{self.class.name} #{parts.join(' ')}>"
|
|
549
|
+
end
|
|
550
|
+
|
|
437
551
|
private
|
|
438
552
|
|
|
439
553
|
# Validate and parse moves array
|
|
@@ -478,6 +592,16 @@ module Sashite
|
|
|
478
592
|
# Return the move tuple with seconds as float
|
|
479
593
|
[pan_notation, seconds.to_f].freeze
|
|
480
594
|
end
|
|
595
|
+
|
|
596
|
+
# Validate draw_offered_by field
|
|
597
|
+
#
|
|
598
|
+
# @param value [String, nil] draw offer value to validate
|
|
599
|
+
# @raise [ArgumentError] if value is invalid
|
|
600
|
+
def validate_draw_offered_by(value)
|
|
601
|
+
return if VALID_DRAW_OFFERED_BY.include?(value)
|
|
602
|
+
|
|
603
|
+
raise ::ArgumentError, ERROR_INVALID_DRAW_OFFERED_BY
|
|
604
|
+
end
|
|
481
605
|
end
|
|
482
606
|
end
|
|
483
607
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sashite-pcn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cyril Kato
|
|
@@ -113,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
113
113
|
- !ruby/object:Gem::Version
|
|
114
114
|
version: '0'
|
|
115
115
|
requirements: []
|
|
116
|
-
rubygems_version: 3.
|
|
116
|
+
rubygems_version: 3.6.9
|
|
117
117
|
specification_version: 4
|
|
118
118
|
summary: PCN (Portable Chess Notation) implementation for Ruby with comprehensive
|
|
119
119
|
game record representation
|