games.gno
23.99 Kb · 910 lines
1package kewrchess_v2
2
3import (
4 "errors"
5 "time"
6
7 "gno.land/p/g1g7dna0gp4nec5rza4q25htj0cjgswrxefp37ep/chess"
8)
9
10// Timeout duration (seconds) for challenge proposals and turn timeouts
11var ChallengeWindowSeconds int64 = 180 // 3-minute challenge window for settlement proposals
12var TurnTimeoutSeconds int64 = 120 // 2-minute move timeout when opponent is AFK (Abandonment)
13
14// getVerifiedCaller validates the format and availability of the caller address.
15func getVerifiedCaller(caller address) (address, error) {
16 if caller == "" {
17 return "", errors.New("alamat caller tidak boleh kosong")
18 }
19 return caller, nil
20}
21
22// Stores all chess games permanently on the blockchain
23var games []*Game
24var nextGameID int
25
26// On-chain matchmaking queue (FIFO)
27var matchmakingQueue []address
28
29// init automatically runs when contract is initialized
30func init() {
31 // Initial demo game setup: White player in Game #0
32 pemberiTantangan := address("g1g7dna0gp4nec5rza4q25htj0cjgswrxefp37ep")
33 penerimaTantangan := address("g1kewrchessplayerblack777777777777777") // Simulated opponent
34
35 newGame := NewGame(nextGameID, pemberiTantangan, penerimaTantangan)
36 games = append(games, newGame)
37 nextGameID++
38}
39
40// CreateGame creates a new direct game challenge
41func CreateGame(cur realm, opponent address) error {
42 caller := cur.Previous().Address()
43 return CreateGameInternal(caller, opponent)
44}
45
46func CreateGameInternal(caller address, opponent address) error {
47 vCaller, err := getVerifiedCaller(caller)
48 if err != nil {
49 return err
50 }
51
52 if vCaller == opponent {
53 return errors.New("tidak bisa bermain melawan diri sendiri")
54 }
55
56 newGame := NewGame(nextGameID, vCaller, opponent)
57 games = append(games, newGame)
58 nextGameID++
59
60 return nil
61}
62
63// JoinQueue registers a player into the on-chain matchmaking queue.
64// If the queue is empty, the player is added to the queue.
65// If another player is already waiting, both players are immediately matched into a new game.
66func JoinQueue(cur realm) error {
67 caller := cur.Previous().Address()
68 return JoinQueueInternal(caller)
69}
70
71func JoinQueueInternal(caller address) error {
72 vCaller, err := getVerifiedCaller(caller)
73 if err != nil {
74 return err
75 }
76
77 // Check if player is already in queue
78 for _, addr := range matchmakingQueue {
79 if addr == vCaller {
80 return errors.New("anda sudah berada di dalam antrean matchmaking")
81 }
82 }
83
84 // If queue is empty, add caller to queue
85 if len(matchmakingQueue) == 0 {
86 matchmakingQueue = append(matchmakingQueue, vCaller)
87 return nil
88 }
89
90 // If a player is waiting, take the first player (FIFO)
91 opponent := matchmakingQueue[0]
92 matchmakingQueue = matchmakingQueue[1:]
93
94 // Create new game: waiting player becomes White, caller becomes Black
95 newGame := NewGame(nextGameID, opponent, vCaller)
96 games = append(games, newGame)
97 nextGameID++
98
99 return nil
100}
101
102// LeaveQueue cancels and removes a player from the on-chain matchmaking queue
103func LeaveQueue(cur realm) error {
104 caller := cur.Previous().Address()
105 return LeaveQueueInternal(caller)
106}
107
108func LeaveQueueInternal(caller address) error {
109 vCaller, err := getVerifiedCaller(caller)
110 if err != nil {
111 return err
112 }
113
114 for i, addr := range matchmakingQueue {
115 if addr == vCaller {
116 matchmakingQueue = append(matchmakingQueue[:i], matchmakingQueue[i+1:]...)
117 return nil
118 }
119 }
120 return errors.New("anda tidak sedang berada di dalam antrean matchmaking")
121}
122
123func makeMoveInternal(caller address, gameID int, fromRow, fromCol, toRow, toCol int, promoPiece string) error {
124 vCaller, err := getVerifiedCaller(caller)
125 if err != nil {
126 return err
127 }
128
129 if gameID < 0 || gameID >= len(games) {
130 return errors.New("game tidak ditemukan")
131 }
132
133 g := games[gameID]
134
135 if g.status != "PLAYING" {
136 return errors.New("game sudah selesai")
137 }
138
139 if g.isWhiteTurn && vCaller != g.white {
140 return errors.New("bukan giliran Anda (Putih)")
141 }
142 if !g.isWhiteTurn && vCaller != g.black {
143 return errors.New("bukan giliran Anda (Hitam)")
144 }
145
146 if fromRow < 0 || fromRow > 7 || fromCol < 0 || fromCol > 7 ||
147 toRow < 0 || toRow > 7 || toCol < 0 || toCol > 7 {
148 return errors.New("koordinat di luar papan")
149 }
150
151 piece := g.board[fromRow][fromCol]
152 if piece == "." {
153 return errors.New("tidak ada bidak di posisi asal")
154 }
155
156 // Delegate move validation to package p/demo/chess
157 if !chess.IsValidMove(g.board, fromRow, fromCol, toRow, toCol, g.isWhiteTurn, g.castling, g.enPassantCol) {
158 return errors.New("langkah catur tidak valid")
159 }
160
161 isWhite := chess.IsWhitePiece(piece)
162
163 // Validate pawn promotion
164 isPromotion := (piece == "P" && toRow == 0) || (piece == "p" && toRow == 7)
165 var finalPiece string = piece
166 if isPromotion {
167 promoted, err := chess.NormalizePromotionPiece(promoPiece, isWhite)
168 if err != nil {
169 return err
170 }
171 finalPiece = promoted
172 }
173
174 isCastling := (piece == "K" || piece == "k") && chess.Abs(toCol-fromCol) == 2
175 isEnPassant := (piece == "P" || piece == "p") && toCol == g.enPassantCol && fromCol != toCol && g.board[toRow][toCol] == "."
176
177 // Verify move does not leave own king in check
178 origDest := g.board[toRow][toCol]
179 g.board[toRow][toCol] = piece
180 g.board[fromRow][fromCol] = "."
181
182 var epRow int
183 var epPiece string
184 if isEnPassant {
185 if isWhite {
186 epRow = 3
187 } else {
188 epRow = 4
189 }
190 epPiece = g.board[epRow][toCol]
191 g.board[epRow][toCol] = "."
192 }
193
194 inCheck := chess.IsKingInCheck(g.board, isWhite)
195
196 // Revert simulation
197 g.board[fromRow][fromCol] = piece
198 g.board[toRow][toCol] = origDest
199 if isEnPassant {
200 g.board[epRow][toCol] = epPiece
201 }
202
203 if inCheck {
204 return errors.New("langkah ilegal: tidak boleh membiarkan Raja Anda terkena skak")
205 }
206
207 // Execute move
208 g.board[toRow][toCol] = finalPiece
209 g.board[fromRow][fromCol] = "."
210
211 // Execute En Passant capture
212 if isEnPassant {
213 if isWhite {
214 g.board[3][toCol] = "."
215 } else {
216 g.board[4][toCol] = "."
217 }
218 }
219
220 // Move Rook during castling
221 if isCastling {
222 if isWhite {
223 if toCol == 6 { // Kingside
224 g.board[7][5] = "R"
225 g.board[7][7] = "."
226 } else if toCol == 2 { // Queenside
227 g.board[7][3] = "R"
228 g.board[7][0] = "."
229 }
230 } else {
231 if toCol == 6 { // Kingside
232 g.board[0][5] = "r"
233 g.board[0][7] = "."
234 } else if toCol == 2 { // Queenside
235 g.board[0][3] = "r"
236 g.board[0][0] = "."
237 }
238 }
239 }
240
241 // Update castling rights
242 if piece == "K" {
243 g.castling.WhiteKingMoved = true
244 } else if piece == "k" {
245 g.castling.BlackKingMoved = true
246 } else if piece == "R" {
247 if fromRow == 7 && fromCol == 0 {
248 g.castling.WhiteLeftRookMoved = true
249 } else if fromRow == 7 && fromCol == 7 {
250 g.castling.WhiteRightRookMoved = true
251 }
252 } else if piece == "r" {
253 if fromRow == 0 && fromCol == 0 {
254 g.castling.BlackLeftRookMoved = true
255 } else if fromRow == 0 && fromCol == 7 {
256 g.castling.BlackRightRookMoved = true
257 }
258 }
259
260 // Update en passant vulnerability
261 if (piece == "P" || piece == "p") && chess.Abs(toRow-fromRow) == 2 {
262 g.enPassantCol = fromCol
263 } else {
264 g.enPassantCol = -1
265 }
266
267 // Detect Checkmate or Stalemate for opponent
268 opponentIsWhite := !isWhite
269 if !chess.HasAnyLegalMoves(g.board, opponentIsWhite, g.castling, g.enPassantCol) {
270 if chess.IsKingInCheck(g.board, opponentIsWhite) {
271 if isWhite {
272 g.status = "WHITE_WON"
273 } else {
274 g.status = "BLACK_WON"
275 }
276 } else {
277 g.status = "DRAW"
278 }
279 }
280
281 g.isWhiteTurn = !g.isWhiteTurn
282 g.drawOfferedBy = ""
283 g.turnDeadline = 0
284 g.turnChallengedBy = ""
285
286 return nil
287}
288
289// MakeMove moves a piece (defaults to Queen on promotion)
290func MakeMove(cur realm, gameID int, fromRow, fromCol, toRow, toCol int) error {
291 caller := cur.Previous().Address()
292 return MakeMoveInternal(caller, gameID, fromRow, fromCol, toRow, toCol)
293}
294
295func MakeMoveInternal(caller address, gameID int, fromRow, fromCol, toRow, toCol int) error {
296 return makeMoveInternal(caller, gameID, fromRow, fromCol, toRow, toCol, "Q")
297}
298
299// MakeMovePromote moves a piece with a specified promotion piece (Q, R, B, N)
300func MakeMovePromote(cur realm, gameID int, fromRow, fromCol, toRow, toCol int, promoPiece string) error {
301 caller := cur.Previous().Address()
302 return MakeMovePromoteInternal(caller, gameID, fromRow, fromCol, toRow, toCol, promoPiece)
303}
304
305func MakeMovePromoteInternal(caller address, gameID int, fromRow, fromCol, toRow, toCol int, promoPiece string) error {
306 return makeMoveInternal(caller, gameID, fromRow, fromCol, toRow, toCol, promoPiece)
307}
308
309// Resign resigns the game for the calling player
310func Resign(cur realm, gameID int) error {
311 caller := cur.Previous().Address()
312 return ResignInternal(caller, gameID)
313}
314
315func ResignInternal(caller address, gameID int) error {
316 vCaller, err := getVerifiedCaller(caller)
317 if err != nil {
318 return err
319 }
320
321 if gameID < 0 || gameID >= len(games) {
322 return errors.New("game tidak ditemukan")
323 }
324
325 g := games[gameID]
326
327 if g.status != "PLAYING" && g.status != "PROPOSED" {
328 return errors.New("game sudah selesai")
329 }
330
331 if vCaller != g.white && vCaller != g.black {
332 return errors.New("hanya pemain dalam game ini yang dapat menyerah")
333 }
334
335 g.resignedBy = vCaller
336 if vCaller == g.white {
337 g.status = "BLACK_WON"
338 } else {
339 g.status = "WHITE_WON"
340 }
341
342 g.settledBy = vCaller
343 g.drawOfferedBy = ""
344 g.turnDeadline = 0
345 g.turnChallengedBy = ""
346 g.challengeDeadline = 0
347 return nil
348}
349
350// OfferDraw offers a draw to the opponent
351func OfferDraw(cur realm, gameID int) error {
352 caller := cur.Previous().Address()
353 return OfferDrawInternal(caller, gameID)
354}
355
356func OfferDrawInternal(caller address, gameID int) error {
357 vCaller, err := getVerifiedCaller(caller)
358 if err != nil {
359 return err
360 }
361
362 if gameID < 0 || gameID >= len(games) {
363 return errors.New("game tidak ditemukan")
364 }
365
366 g := games[gameID]
367
368 if g.status != "PLAYING" {
369 return errors.New("game sudah selesai")
370 }
371
372 if vCaller != g.white && vCaller != g.black {
373 return errors.New("hanya pemain dalam game ini yang dapat menawarkan remis")
374 }
375
376 if g.drawOfferedBy == vCaller {
377 return errors.New("anda sudah mengajukan tawaran remis")
378 }
379
380 if g.drawOfferedBy != "" && g.drawOfferedBy != vCaller {
381 g.status = "DRAW"
382 g.drawOfferedBy = ""
383 return nil
384 }
385
386 g.drawOfferedBy = vCaller
387 return nil
388}
389
390// AcceptDraw accepts a draw offer from the opponent
391func AcceptDraw(cur realm, gameID int) error {
392 caller := cur.Previous().Address()
393 return AcceptDrawInternal(caller, gameID)
394}
395
396func AcceptDrawInternal(caller address, gameID int) error {
397 vCaller, err := getVerifiedCaller(caller)
398 if err != nil {
399 return err
400 }
401
402 if gameID < 0 || gameID >= len(games) {
403 return errors.New("game tidak ditemukan")
404 }
405
406 g := games[gameID]
407
408 if g.status != "PLAYING" {
409 return errors.New("game sudah selesai")
410 }
411
412 if vCaller != g.white && vCaller != g.black {
413 return errors.New("hanya pemain dalam game ini yang dapat menerima remis")
414 }
415
416 if g.drawOfferedBy == "" {
417 return errors.New("tidak ada tawaran remis yang sedang aktif")
418 }
419
420 if g.drawOfferedBy == vCaller {
421 return errors.New("tidak bisa menerima tawaran remis yang Anda ajukan sendiri")
422 }
423
424 g.status = "DRAW"
425 g.drawOfferedBy = ""
426 return nil
427}
428
429// DeclineDraw declines a draw offer from the opponent
430func DeclineDraw(cur realm, gameID int) error {
431 caller := cur.Previous().Address()
432 return DeclineDrawInternal(caller, gameID)
433}
434
435func DeclineDrawInternal(caller address, gameID int) error {
436 vCaller, err := getVerifiedCaller(caller)
437 if err != nil {
438 return err
439 }
440
441 if gameID < 0 || gameID >= len(games) {
442 return errors.New("game tidak ditemukan")
443 }
444
445 g := games[gameID]
446
447 if g.status != "PLAYING" {
448 return errors.New("game sudah selesai")
449 }
450
451 if vCaller != g.white && vCaller != g.black {
452 return errors.New("hanya pemain dalam game ini yang dapat menolak remis")
453 }
454
455 if g.drawOfferedBy == "" {
456 return errors.New("tidak ada tawaran remis yang sedang aktif")
457 }
458
459 if g.drawOfferedBy == vCaller {
460 return errors.New("tidak bisa menolak tawaran remis yang Anda ajukan sendiri")
461 }
462
463 g.drawOfferedBy = ""
464 return nil
465}
466
467// ProposeSettlement submits a victory claim for off-chain chess results.
468// Prevents fraudulent opponent moves: proposal opens a challenge period.
469// Opponent can confirm immediately (ConfirmSettlement), or dispute if cheated (DisputeSettlement).
470// If opponent does not respond, winner can finalize after ChallengeWindowSeconds expires.
471// ProposeSettlement submits a victory claim for off-chain chess results.
472// Prevents fraudulent opponent moves: proposal opens a challenge period.
473// Opponent can confirm immediately (ConfirmSettlement), or dispute if cheated (DisputeSettlement).
474// If opponent does not respond, winner can finalize after ChallengeWindowSeconds expires.
475func ProposeSettlement(cur realm, gameID int, movesUCI string) error {
476 caller := cur.Previous().Address()
477 return ProposeSettlementInternal(caller, gameID, movesUCI)
478}
479
480func ProposeSettlementInternal(caller address, gameID int, movesUCI string) error {
481 vCaller, err := getVerifiedCaller(caller)
482 if err != nil {
483 return err
484 }
485
486 if gameID < 0 || gameID >= len(games) {
487 return errors.New("game tidak ditemukan")
488 }
489
490 g := games[gameID]
491
492 if vCaller != g.white && vCaller != g.black {
493 return errors.New("hanya pemain dalam game ini yang dapat mengajukan settlement")
494 }
495
496 if g.settledBy != "" {
497 return errors.New("game ini sudah diselesaikan di blockchain")
498 }
499
500 if g.status != "PLAYING" {
501 return errors.New("game ini sedang dalam status " + g.status)
502 }
503
504 // Replay chess moves from the beginning using package gno.land/p/demo/chess
505 finalBoard, _, status, err := chess.ReplayUCI(movesUCI)
506 if err != nil {
507 return errors.New("verifikasi langkah catur gagal: " + err.Error())
508 }
509
510 if status == "PLAYING" {
511 return errors.New("rangkaian langkah catur belum menghasilkan skakmat atau remis")
512 }
513
514 // Verify winner claim rights
515 if status == "WHITE_WON" && vCaller != g.white {
516 return errors.New("hanya pemain Putih yang berhak klaim kemenangan untuk White")
517 }
518 if status == "BLACK_WON" && vCaller != g.black {
519 return errors.New("hanya pemain Hitam yang berhak klaim kemenangan untuk Black")
520 }
521
522 g.board = finalBoard
523 g.proposedBy = vCaller
524 g.pendingStatus = status
525 g.challengeDeadline = time.Now().Unix() + ChallengeWindowSeconds
526 g.status = "PROPOSED"
527 g.drawOfferedBy = ""
528 g.turnDeadline = 0
529 g.turnChallengedBy = ""
530
531 return nil
532}
533
534// ConfirmSettlement allows opponent to immediately approve final result without waiting for challenge window
535func ConfirmSettlement(cur realm, gameID int) error {
536 caller := cur.Previous().Address()
537 return ConfirmSettlementInternal(caller, gameID)
538}
539
540func ConfirmSettlementInternal(caller address, gameID int) error {
541 vCaller, err := getVerifiedCaller(caller)
542 if err != nil {
543 return err
544 }
545
546 if gameID < 0 || gameID >= len(games) {
547 return errors.New("game tidak ditemukan")
548 }
549
550 g := games[gameID]
551
552 if g.status != "PROPOSED" {
553 return errors.New("tidak ada proposal settlement yang sedang aktif")
554 }
555
556 if vCaller != g.white && vCaller != g.black {
557 return errors.New("hanya pemain dalam game ini yang dapat mengonfirmasi")
558 }
559
560 if vCaller == g.proposedBy {
561 return errors.New("tidak bisa mengonfirmasi proposal yang Anda ajukan sendiri")
562 }
563
564 g.status = g.pendingStatus
565 g.settledBy = g.proposedBy
566 g.challengeDeadline = 0
567 return nil
568}
569
570// FinalizeSettlement officially finalizes the game after the challenge period has passed
571func FinalizeSettlement(cur realm, gameID int) error {
572 caller := cur.Previous().Address()
573 return FinalizeSettlementInternal(caller, gameID)
574}
575
576func FinalizeSettlementInternal(caller address, gameID int) error {
577 vCaller, err := getVerifiedCaller(caller)
578 if err != nil {
579 return err
580 }
581
582 if gameID < 0 || gameID >= len(games) {
583 return errors.New("game tidak ditemukan")
584 }
585
586 g := games[gameID]
587
588 if g.status != "PROPOSED" {
589 return errors.New("tidak ada proposal settlement yang sedang aktif")
590 }
591
592 if vCaller != g.white && vCaller != g.black {
593 return errors.New("hanya pemain dalam game ini yang dapat memfinalisasi")
594 }
595
596 now := time.Now().Unix()
597 if now < g.challengeDeadline {
598 return errors.New("jendela sanggahan (challenge period) belum berakhir")
599 }
600
601 g.status = g.pendingStatus
602 g.settledBy = g.proposedBy
603 g.challengeDeadline = 0
604 return nil
605}
606
607// DisputeSettlement allows opponent to dispute a fraudulent settlement proposal
608func DisputeSettlement(cur realm, gameID int) error {
609 caller := cur.Previous().Address()
610 return DisputeSettlementInternal(caller, gameID)
611}
612
613func DisputeSettlementInternal(caller address, gameID int) error {
614 vCaller, err := getVerifiedCaller(caller)
615 if err != nil {
616 return err
617 }
618
619 if gameID < 0 || gameID >= len(games) {
620 return errors.New("game tidak ditemukan")
621 }
622
623 g := games[gameID]
624
625 if g.status != "PROPOSED" {
626 return errors.New("tidak ada proposal settlement yang sedang aktif")
627 }
628
629 if vCaller != g.white && vCaller != g.black {
630 return errors.New("hanya pemain dalam game ini yang dapat menyanggah")
631 }
632
633 if vCaller == g.proposedBy {
634 return errors.New("tidak bisa menyanggah proposal yang Anda ajukan sendiri")
635 }
636
637 // Cancel fraudulent proposal and revert game status back to PLAYING
638 g.status = "PLAYING"
639 g.proposedBy = ""
640 g.pendingStatus = ""
641 g.challengeDeadline = 0
642 return nil
643}
644
645// SettleGame proposes settlement and finalizes immediately if challenge window is 0
646func SettleGame(cur realm, gameID int, movesUCI string) error {
647 caller := cur.Previous().Address()
648 return SettleGameInternal(caller, gameID, movesUCI)
649}
650
651func SettleGameInternal(caller address, gameID int, movesUCI string) error {
652 err := ProposeSettlementInternal(caller, gameID, movesUCI)
653 if err != nil {
654 return err
655 }
656 if ChallengeWindowSeconds <= 0 {
657 return FinalizeSettlementInternal(caller, gameID)
658 }
659 return nil
660}
661
662// StartTurnTimer is initiated when opponent goes AFK or closes browser (Rage Quit) during an off-chain game.
663// Player submits current move history (currentMovesUCI).
664// Smart contract validates the board and ensures it is the OPPONENT'S TURN.
665func StartTurnTimer(cur realm, gameID int, currentMovesUCI string) error {
666 caller := cur.Previous().Address()
667 return StartTurnTimerInternal(caller, gameID, currentMovesUCI)
668}
669
670func StartTurnTimerInternal(caller address, gameID int, currentMovesUCI string) error {
671 vCaller, err := getVerifiedCaller(caller)
672 if err != nil {
673 return err
674 }
675
676 if gameID < 0 || gameID >= len(games) {
677 return errors.New("game tidak ditemukan")
678 }
679
680 g := games[gameID]
681
682 if g.status != "PLAYING" {
683 return errors.New("game sudah selesai")
684 }
685
686 if vCaller != g.white && vCaller != g.black {
687 return errors.New("hanya pemain dalam game ini yang dapat menuntut timer giliran")
688 }
689
690 currentBoard, nextIsWhiteTurn, status, err := chess.ReplayUCI(currentMovesUCI)
691 if err != nil {
692 return errors.New("verifikasi langkah posisi saat ini gagal: " + err.Error())
693 }
694
695 if status != "PLAYING" {
696 return errors.New("posisi catur ini sudah selesai (skakmat/remis), gunakan SettleGame")
697 }
698
699 // Validate that next turn is the opponent's turn, not the caller's
700 if vCaller == g.white && nextIsWhiteTurn {
701 return errors.New("saat ini adalah giliran Anda sendiri (Putih), lakukan langkah Anda terlebih dahulu")
702 }
703 if vCaller == g.black && !nextIsWhiteTurn {
704 return errors.New("saat ini adalah giliran Anda sendiri (Hitam), lakukan langkah Anda terlebih dahulu")
705 }
706
707 g.board = currentBoard
708 g.isWhiteTurn = nextIsWhiteTurn
709 g.turnChallengedBy = vCaller
710 g.turnDeadline = time.Now().Unix() + TurnTimeoutSeconds
711
712 return nil
713}
714
715// ClaimTimeout allows the challenger to win the game after the opponent goes AFK/rage quits
716// and turnDeadline has passed without a response move from the opponent.
717func ClaimTimeout(cur realm, gameID int) error {
718 caller := cur.Previous().Address()
719 return ClaimTimeoutInternal(caller, gameID)
720}
721
722func ClaimTimeoutInternal(caller address, gameID int) error {
723 vCaller, err := getVerifiedCaller(caller)
724 if err != nil {
725 return err
726 }
727
728 if gameID < 0 || gameID >= len(games) {
729 return errors.New("game tidak ditemukan")
730 }
731
732 g := games[gameID]
733
734 if g.status != "PLAYING" {
735 return errors.New("game sudah selesai")
736 }
737
738 if g.turnDeadline == 0 {
739 return errors.New("tidak ada batas waktu giliran yang sedang berjalan")
740 }
741
742 now := time.Now().Unix()
743 if now < g.turnDeadline {
744 return errors.New("batas waktu giliran lawan belum habis")
745 }
746
747 if vCaller != g.turnChallengedBy {
748 return errors.New("hanya pemain yang menuntut timer yang dapat mengklaim kemenangan timeout")
749 }
750
751 if vCaller == g.white {
752 g.status = "WHITE_WON"
753 } else {
754 g.status = "BLACK_WON"
755 }
756
757 g.settledBy = vCaller
758 g.turnDeadline = 0
759 g.turnChallengedBy = ""
760
761 return nil
762}
763
764// GetGamesCount returns the total number of games
765func GetGamesCount() int {
766 return len(games)
767}
768
769// GetBoard returns the 8x8 board for a given game ID
770func GetBoard(gameID int) [8][8]string {
771 if gameID < 0 || gameID >= len(games) {
772 panic("game tidak ditemukan")
773 }
774 return [8][8]string(games[gameID].board)
775}
776
777// GetWhitePlayer returns the White player address for a given game ID
778func GetWhitePlayer(gameID int) address {
779 if gameID < 0 || gameID >= len(games) {
780 panic("game tidak ditemukan")
781 }
782 return games[gameID].white
783}
784
785// GetBlackPlayer returns the Black player address for a given game ID
786func GetBlackPlayer(gameID int) address {
787 if gameID < 0 || gameID >= len(games) {
788 panic("game tidak ditemukan")
789 }
790 return games[gameID].black
791}
792
793// IsWhiteTurn returns whether it is White's turn for a given game ID
794func IsWhiteTurn(gameID int) bool {
795 if gameID < 0 || gameID >= len(games) {
796 panic("game tidak ditemukan")
797 }
798 return games[gameID].isWhiteTurn
799}
800
801// GetGameStatus returns the game status for a given game ID
802func GetGameStatus(gameID int) string {
803 if gameID < 0 || gameID >= len(games) {
804 panic("game tidak ditemukan")
805 }
806 return games[gameID].status
807}
808
809// GetDrawOfferedBy returns the address that offered a draw, or empty address if none
810func GetDrawOfferedBy(gameID int) address {
811 if gameID < 0 || gameID >= len(games) {
812 panic("game tidak ditemukan")
813 }
814 return games[gameID].drawOfferedBy
815}
816
817// GetResignedBy returns the address of the player who resigned, or empty address if none
818func GetResignedBy(gameID int) address {
819 if gameID < 0 || gameID >= len(games) {
820 panic("game tidak ditemukan")
821 }
822 return games[gameID].resignedBy
823}
824
825// GetQueueCount returns the number of players waiting in the matchmaking queue
826func GetQueueCount() int {
827 return len(matchmakingQueue)
828}
829
830// IsInQueue checks if an address is currently waiting in the matchmaking queue
831func IsInQueue(addr address) bool {
832 for _, a := range matchmakingQueue {
833 if a == addr {
834 return true
835 }
836 }
837 return false
838}
839
840// GetActiveGameForPlayer searches for the latest active game ("PLAYING") for a given address.
841// Returns -1 if no active game found.
842func GetActiveGameForPlayer(addr address) int {
843 for i := len(games) - 1; i >= 0; i-- {
844 g := games[i]
845 if (g.white == addr || g.black == addr) && g.status == "PLAYING" {
846 return g.id
847 }
848 }
849 return -1
850}
851
852// GetLatestGameForPlayer returns the latest game ID involving the player, or -1 if none.
853func GetLatestGameForPlayer(addr address) int {
854 for i := len(games) - 1; i >= 0; i-- {
855 g := games[i]
856 if g.white == addr || g.black == addr {
857 return g.id
858 }
859 }
860 return -1
861}
862
863// GetSettledBy returns the address that finalized the game settlement on-chain
864func GetSettledBy(gameID int) address {
865 if gameID < 0 || gameID >= len(games) {
866 panic("game tidak ditemukan")
867 }
868 return games[gameID].settledBy
869}
870
871// GetProposedBy returns the address that proposed settlement
872func GetProposedBy(gameID int) address {
873 if gameID < 0 || gameID >= len(games) {
874 panic("game tidak ditemukan")
875 }
876 return games[gameID].proposedBy
877}
878
879// GetPendingStatus returns the pending status of settlement
880func GetPendingStatus(gameID int) string {
881 if gameID < 0 || gameID >= len(games) {
882 panic("game tidak ditemukan")
883 }
884 return games[gameID].pendingStatus
885}
886
887// GetChallengeDeadline returns the deadline unix timestamp for challenging settlement
888func GetChallengeDeadline(gameID int) int64 {
889 if gameID < 0 || gameID >= len(games) {
890 panic("game tidak ditemukan")
891 }
892 return games[gameID].challengeDeadline
893}
894
895// GetTurnDeadline returns the deadline unix timestamp for turn timeout
896func GetTurnDeadline(gameID int) int64 {
897 if gameID < 0 || gameID >= len(games) {
898 panic("game tidak ditemukan")
899 }
900 return games[gameID].turnDeadline
901}
902
903// GetTurnChallengedBy returns the address that initiated the turn timer
904func GetTurnChallengedBy(gameID int) address {
905 if gameID < 0 || gameID >= len(games) {
906 panic("game tidak ditemukan")
907 }
908 return games[gameID].turnChallengedBy
909}
910