shithub: chessfs

ref: 470f19bc73180395683aa1e14c96ea3c2d3a828f
dir: /cmd/chessterm/chessterm.go/

View raw version
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)
		}
	}
}