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

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