Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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