ref: eb8f0cbd6b03c83d0ec53c73ce721dae347217de
dir: /cmd/chessterm/chessterm.go/
package main import ( "fmt" "flag" "log" "os" "strconv" "strings" "time" "github.com/notnil/chess" "9fans.net/go/draw" "disroot.org/kitzman/chessfs/pieces" ) const ( BoardWidth = 640 BoardHeight = 704 BoardD = 64 TickerDuration = "0.5s" ) var ( player string gameDir string bgcol *draw.Image fgcol *draw.Image squarelcol *draw.Image squaredcol *draw.Image piecewcol *draw.Image piecebcol *draw.Image display *draw.Display screen *draw.Image mousectl *draw.Mousectl kbdctl *draw.Keyboardctl ) func allocColors(bgcolf, fgcolf string) error { rgb, err := draw.ParsePix("r8g8b8") if err != nil { return err } bgcode, err := strconv.ParseUint(bgcolf, 0, 32) if err != nil { return err } fgcode, err := strconv.ParseUint(fgcolf, 0, 32) if err != nil { return err } bgcolr := draw.Color(uint32(bgcode) << 8 | 0xff) fgcolr := draw.Color(uint32(fgcode) << 8 | 0xff) var lightcolr = bgcolr var darkcolr = fgcolr if ((uint32(bgcode) >> 0) & 0xff) + ((uint32(bgcode) >> 8) & 0xff) + ((uint32(bgcode) >> 8) & 0xff) > ((uint32(fgcode) >> 0) & 0xff) + ((uint32(fgcode) >> 8) & 0xff) + ((uint32(fgcode) >> 8) & 0xff) { lightcolr = fgcolr darkcolr = bgcolr } bgcol, err = display.AllocImage(draw.Rect(0, 0, 1, 1), rgb, true, bgcolr) fgcol, err = display.AllocImage(draw.Rect(0, 0, 1, 1), rgb, true, fgcolr) squarelcol = display.AllocImageMix(darkcolr, lightcolr) squaredcol = display.AllocImageMix(lightcolr, darkcolr) piecewcol, err = display.AllocImage(draw.Rect(0, 0, 1, 1), rgb, true, lightcolr) piecebcol, err = display.AllocImage(draw.Rect(0, 0, 1, 1), rgb, true, darkcolr) return nil } func drawStatus(status GameStatus, redraw bool) { off := float64(BoardD / 4) // game status and errors if redraw || status.Status != status.LastStatus.Status { x := int(BoardD * 1 + off) y := int(BoardD * 9 + off) xe := int(BoardD * 4) ye := int(BoardD * 10) screen.Draw(draw.Rect(x, y, xe, ye), bgcol, nil, draw.ZP) screen.String(draw.Pt(x, y), fgcol, draw.Pt(xe, ye), display.Font, status.Status) } if redraw || status.Msg != status.LastStatus.Msg { x := int(BoardD * 1 + off) y := int(BoardD * 10 + off) xe := int(BoardD * 4) ye := int(BoardD * 11) screen.Draw(draw.Rect(x, y, xe, ye), bgcol, nil, draw.ZP) screen.String(draw.Pt(x, y), fgcol, draw.Pt(xe, ye), display.Font, status.Msg) } // players' time if redraw || status.TimeWhite != status.LastStatus.TimeWhite { x := int(BoardD * 6 + off) y := int(BoardD * 9 + off) xe := int(BoardD * 9) ye := int(BoardD * 10) screen.Draw(draw.Rect(x, y, xe, ye), bgcol, nil, draw.ZP) screen.String(draw.Pt(x, y), fgcol, draw.Pt(xe, ye), display.Font, fmt.Sprintf("W %d", status.TimeWhite)) } if redraw || status.TimeBlack != status.LastStatus.TimeBlack { x := int(BoardD * 6 + off) y := int(BoardD * 10 + off) xe := int(BoardD * 9) ye := int(BoardD * 11) screen.Draw(draw.Rect(x, y, xe, ye), bgcol, nil, draw.ZP) screen.String(draw.Pt(x, y), fgcol, draw.Pt(xe, ye), display.Font, fmt.Sprintf("B %d", status.TimeBlack)) } } func drawSquare(i, j int, status GameStatus, ra bool) { var col *draw.Image var x = BoardD * (i + 1) var y = BoardD * (j + 1) var redraw = ra rgba, _ := draw.ParsePix("a8r8g8b8") boardSquare := Selection{x: i, y: j}.ToSquare() var square = draw.Rect(x, y, x + BoardD, y + BoardD) // var moveColor = 0 // if player == "black" { // moveColor = 1 // } switch (int(i) + int(j)) % 2 { case 0: col = squaredcol case 1: col = squarelcol } if redraw || status.Board[boardSquare] != status.LastStatus.Board[boardSquare] { screen.Draw(square, col, nil, draw.ZP) } // selected square if status.Selected != nil && status.Selected.x == i && status.Selected.y == j { d := BoardD / 8 marksql := draw.Rect(x + d, y + d, x + d * 2, y + d * 2) marksqr := draw.Rect(x + BoardD - d * 2, y + d, x + BoardD - d, y + d * 2) screen.Draw(marksql, fgcol, nil, draw.ZP) screen.Draw(marksqr, bgcol, nil, draw.ZP) } // draw piece x = BoardD * (i + 1) + BoardD / 8 y = BoardD * (j + 1) + BoardD / 8 if !redraw && status.Board[boardSquare] == status.LastStatus.Board[boardSquare] { return } square = draw.Rect(0, 0, pieces.PieceSize, pieces.PieceSize) piece, err := display.AllocImage(square, rgba, true, draw.Transparent) switch status.Board[boardSquare].Color() { case chess.White: switch status.Board[boardSquare].Type() { case chess.King: _, err = piece.Load(square, pieces.WhiteKing) case chess.Queen: _, err = piece.Load(square, pieces.WhiteQueen) case chess.Rook: _, err = piece.Load(square, pieces.WhiteRook) case chess.Bishop: _, err = piece.Load(square, pieces.WhiteBishop) case chess.Knight: _, err = piece.Load(square, pieces.WhiteKnight) case chess.Pawn: _, err = piece.Load(square, pieces.WhitePawn) } case chess.Black: switch status.Board[boardSquare].Type() { case chess.King: _, err = piece.Load(square, pieces.BlackKing) case chess.Queen: _, err = piece.Load(square, pieces.BlackQueen) case chess.Rook: _, err = piece.Load(square, pieces.BlackRook) case chess.Bishop: _, err = piece.Load(square, pieces.BlackBishop) case chess.Knight: _, err = piece.Load(square, pieces.BlackKnight) case chess.Pawn: _, err = piece.Load(square, pieces.BlackPawn) } } if err != nil { log.Printf("error loading piece image: %v", err) return } screen.Draw(draw.Rect(x, y, x + pieces.PieceSize, y + pieces.PieceSize), piece, nil, draw.ZP) } func drawBoard(status GameStatus, redraw bool) { off := BoardD / 4 var marker int // fill screen if redraw { screen.Draw(screen.R, bgcol, nil, draw.ZP) } // draw squares for i := 0; i < 8; i++ { for j := 0; j < 8; j++ { drawSquare(i, j, status, redraw) } } if redraw { // draw notation for i := 1; i < 9; i++ { x := BoardD * i if player == "white" { marker = 'A' + i - 1 } else { marker = 'H' - i + 1 } screen.String(draw.Pt(x + off, 0 + off), fgcol, draw.Pt(x + BoardD, 0 + BoardD), display.Font, string(marker)) } for i := 1; i < 9; i++ { y := BoardD * i if player == "white" { marker = '9' - i } else { marker = '0' + i } screen.String(draw.Pt(0 + off, y + off), fgcol, draw.Pt(0 + BoardD, y + BoardD), display.Font, string(marker)) } for i := 1; i < 9; i++ { y := BoardD * i if player == "white" { marker = '9' - i } else { marker = '0' + i } screen.String(draw.Pt(0 + BoardD * 9 + off, y + off), fgcol, draw.Pt(0 + BoardD * 10, y + BoardD), display.Font, string(marker)) } } drawStatus(status, redraw) } func getClickedSquare(x, y int) *Selection { sqx := x / BoardD sqy := y / BoardD var sx int var sy int if sqx > 0 && sqx < 9 { sx = sqx - 1 } else { return nil } if sqy > 0 && sqy < 9 { sy = sqy - 1 } else { return nil } sel := Selection{ x: sx, y: sy } return &sel } type GameStatus struct { Status string Msg string TimeWhite int TimeBlack int Game *chess.Game Board map[chess.Square]chess.Piece Selected *Selection LastStatus *GameStatus } type Selection struct { x, y int } func (s Selection) ToSquare() chess.Square { if player == "white" { return chess.NewSquare(chess.File(s.x), chess.Rank(7 - s.y)) } return chess.NewSquare(chess.File(7 - s.x), chess.Rank(s.y)) } func (s GameStatus) Equals(n GameStatus) bool { var isSame = true isSame = isSame && s.Status == n.Status isSame = isSame && s.Msg == n.Msg isSame = isSame && s.TimeWhite == n.TimeWhite isSame = isSame && s.TimeBlack == n.TimeBlack // isSame = isSame && s.Selected == n.Selected for i := 0; i < 7; i++ { for j := 0; j < 7; j++ { square := chess.NewSquare(chess.File(i), chess.Rank(j)) isSame = isSame && s.Board[square] == n.Board[square] } } return isSame } func GameWatcher(upch chan GameStatus) { timerDuration, err := time.ParseDuration(TickerDuration) if err != nil { log.Fatalf("watcher: malformed duration: %v\n", err) os.Exit(1) } timer := time.NewTicker(timerDuration) ctlFile := gameDir + "/ctl" fenFile := gameDir + "/fen" for { <-timer.C ctl, err := os.ReadFile(ctlFile) if err != nil { log.Fatalf("watcher: %v\n", err) return } moves, err := os.ReadFile(fenFile) if err != nil { log.Fatalf("watcher: %v\n", err) return } ctllines := strings.Split(string(ctl), "\n") status := ctllines[0] msg := ctllines[1] if len(strings.Fields(ctllines[2])) != 3 || len(strings.Fields(ctllines[3])) != 3 { log.Fatalf("watcher: ctl file format is bogus\n") return } timeWhite, err := strconv.Atoi(strings.Fields(ctllines[2])[2]) if err != nil { log.Fatalf("watcher: %v\n", err) return } timeBlack, err := strconv.Atoi(strings.Fields(ctllines[3])[2]) if err != nil { log.Fatalf("watcher: %v\n", err) return } fen, err := chess.FEN(string(moves)) if err != nil { log.Fatalf("watcher: unable to read fen: %v\n", err) return } game := chess.NewGame(fen) board := game.Position().Board().SquareMap() gameStatus := GameStatus { Status: status, Msg: msg, TimeWhite: timeWhite, TimeBlack: timeBlack, Game: game, Board: board, LastStatus: nil, } upch<- gameStatus } } const ( CmdStart = iota CmdSetTime CmdDraw CmdResign CmdMove ) type Command struct { Cmdtype int Timeamt int Move string } type MoveInquiry struct { S1 chess.Square S2 chess.Square Promo chess.PieceType } func GameCommands(cmdch chan Command) { var err error ctlFile := gameDir + "/ctl" boardFile := gameDir + "/" + player omode := os.FileMode(os.O_WRONLY) for { select { case cmd := <- cmdch: err = nil switch cmd.Cmdtype { case CmdStart: err = os.WriteFile(ctlFile, []byte("start"), omode) case CmdSetTime: cmdstr := fmt.Sprintf("time %d", cmd.Timeamt) err = os.WriteFile(ctlFile, []byte(cmdstr), omode) case CmdDraw: err = os.WriteFile(boardFile, []byte("draw"), omode) case CmdResign: err = os.WriteFile(boardFile, []byte("resign"), omode) case CmdMove: err = os.WriteFile(boardFile, []byte(cmd.Move), omode) } if err != nil { log.Printf("command error: %v\n", err) } } } } func AttemptMove(cmdch chan Command, s GameStatus, inq MoveInquiry) { var m *chess.Move n := chess.AlgebraicNotation{} possibleMoves := s.Game.ValidMoves() for _, pm := range(possibleMoves) { var is = true is = is && pm.S1() == inq.S1 is = is && pm.S2() == inq.S2 is = is && pm.Promo() == inq.Promo if is { m = pm break } } if m == nil { return } move := n.Encode(s.Game.Position(), m) cmd := Command { Cmdtype: CmdMove, Timeamt: 0, Move: move, } cmdch<- cmd } const ( MenuStart = "start" MenuDraw = "offer draw" MenuResign = "resign" // MenuTime = "set game time" ) const ( MenuStartHit = iota MenuResignHit MenuDrawHit // MenuTimeHit ) const ( PromoQueen = "queen" PromoRook = "rook" PromoBishop = "bishop" PromoKnight = "knight" ) const ( PromoQueenHit = iota PromoRookHit PromoBishopHit PromoKnightHit ) func main() { fontf := flag.String("font", "", "font path") bgcolf := flag.String("bg", "0xffffff", "background color") fgcolf := flag.String("fg", "0x000000", "foreground color") flag.Parse() if flag.NArg() != 2 { log.Fatalf("game directory or player not supplied or extraneous arguments\n") os.Exit(1) } player = flag.Args()[0] gameDir = flag.Args()[1] if player != "white" && player != "black" { log.Fatalf("player %s unknown\n", player) os.Exit(1) } var err error var font string font = *fontf if font == "" { font = os.Getenv("font") } else { os.Setenv("font", font) } if font == "" { log.Fatal("font environment variable not set") } wsize := fmt.Sprintf("%dx%d", BoardWidth, BoardHeight) errch := make(chan error) display, err = draw.Init(errch, font, "chessterm", wsize) if err != nil { log.Fatal(err) } err = allocColors(*bgcolf, *fgcolf) if err != nil { log.Fatalf("color error: %v", err) os.Exit(1) } screen = display.ScreenImage mousectl = display.InitMouse() kbdctl = display.InitKeyboard() defer display.Close() menu := &draw.Menu { Item: []string { MenuStart, MenuDraw, MenuResign, // MenuTime, }, } promoMenu := &draw.Menu { Item: []string { PromoQueen, PromoRook, PromoBishop, PromoKnight, }, } upch := make(chan GameStatus) cmdch := make(chan Command) go GameWatcher(upch) go GameCommands(cmdch) status := <-upch status.LastStatus = &status drawBoard(status, true) display.Flush() for { select { case newStatus := <-upch: if !newStatus.Equals(status) { newStatus.LastStatus = &status status.LastStatus = nil newStatus.Selected = status.Selected drawBoard(newStatus, false) display.Flush() status = newStatus } case mouse := <-mousectl.C: if mouse.Buttons & 2 == 2 { mcmd := draw.MenuHit(2, mousectl, menu, nil) var cmd = Command { Cmdtype: CmdDraw, Timeamt: 0, Move: "", } switch mcmd { case MenuDrawHit: cmd.Cmdtype = CmdDraw case MenuResignHit: cmd.Cmdtype = CmdResign case MenuStartHit: cmd.Cmdtype = CmdStart // case MenuTimeHit: // cmd.Cmdtype = CmdSetTime // cmd.Timeamt = 500 } cmdch<- cmd } if mouse.Buttons & 1 != 1 { continue } clickedSquare := getClickedSquare(mouse.X, mouse.Y) if clickedSquare == nil { continue } previousSelected := status.Selected if status.Selected != nil && status.Selected.x == clickedSquare.x && status.Selected.y == clickedSquare.y { status.Selected = nil } else { if previousSelected == nil { sq := clickedSquare.ToSquare() sqValue, sqOk := status.Board[sq] if !sqOk || strings.ToLower(sqValue.Color().Name()) != player { continue } } status.Selected = clickedSquare } if previousSelected != nil && status.Selected != nil { s1 := previousSelected.ToSquare() s2 := status.Selected.ToSquare() var promoRank = 0 var pt = chess.NoPieceType switch player { case "white": promoRank = 7 case "black": promoRank = 0 } if s2.Rank() == chess.Rank(promoRank) && status.Board[s1].Type() == chess.Pawn { mpt := draw.MenuHit(1, mousectl, promoMenu, nil) switch mpt { case PromoQueenHit: pt = chess.Queen case PromoRookHit: pt = chess.Rook case PromoBishopHit: pt = chess.Bishop case PromoKnightHit: pt = chess.Knight } } move := MoveInquiry { S1: s1, S2: s2, Promo: pt, } status.Selected = nil go AttemptMove(cmdch, status, move) } if previousSelected != nil { drawSquare(previousSelected.x, previousSelected.y, status, true) } if status.Selected != nil { drawSquare(status.Selected.x, status.Selected.y, status, true) } display.Flush() case <-mousectl.Resize: err := display.Attach(draw.RefNone) if err != nil { errch<- err } screen = display.ScreenImage drawBoard(status, true) display.Flush() case r := <-kbdctl.C: switch r { case 'q': return case ' ': drawBoard(status, true) display.Flush() } case e := <-errch: display.Close() log.Fatalf("draw: %v\n", e) os.Exit(1) } } }