Online backgammon client (FIBS)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

423 lines
8.4 KiB

package game
import (
"bytes"
"embed"
"fmt"
"image"
"image/color"
_ "image/png"
"log"
"strings"
"time"
"code.rocketnine.space/tslocum/fibs"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/nfnt/resize"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
)
// Copyright 2020 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:embed assets
var assetsFS embed.FS
var debugExtra []byte
var (
imgCheckerWhite *ebiten.Image
imgCheckerBlack *ebiten.Image
mplusNormalFont font.Face
mplusBigFont font.Face
)
const defaultServerAddress = "fibs.com:4321"
func init() {
loadAssets(0)
initializeFonts()
}
func loadAssets(width int) {
imgCheckerWhite = loadAsset("assets/checker_white.png", width)
imgCheckerBlack = loadAsset("assets/checker_black.png", width)
}
func loadAsset(assetPath string, width int) *ebiten.Image {
f, err := assetsFS.Open(assetPath)
if err != nil {
panic(err)
}
img, _, err := image.Decode(f)
if err != nil {
log.Fatal(err)
}
if width > 0 {
imgResized := resize.Resize(uint(width), 0, img, resize.Lanczos3)
return ebiten.NewImageFromImage(imgResized)
}
return ebiten.NewImageFromImage(img)
}
func initializeFonts() {
tt, err := opentype.Parse(fonts.MPlus1pRegular_ttf)
if err != nil {
log.Fatal(err)
}
const dpi = 72
mplusNormalFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
Size: 24,
DPI: dpi,
Hinting: font.HintingFull,
})
if err != nil {
log.Fatal(err)
}
mplusBigFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
Size: 32,
DPI: dpi,
Hinting: font.HintingFull,
})
if err != nil {
log.Fatal(err)
}
}
type Sprite struct {
image *ebiten.Image
w int
h int
x int
y int
colorWhite bool
}
func (s *Sprite) Update() {
return // TODO
}
type Sprites struct {
sprites []*Sprite
num int
}
func (s *Sprites) Update() {
for i := 0; i < s.num; i++ {
s.sprites[i].Update()
}
}
const (
MinSprites = 0
MaxSprites = 50000
)
var spinner = []byte(`-\|/`)
type Game struct {
touchIDs []ebiten.TouchID
sprites Sprites
op *ebiten.DrawImageOptions
Board *board
screenW, screenH int
drawBuffer bytes.Buffer
spinnerIndex int
ServerAddress string
Username string
Password string
loggedIn bool
usernameConfirmed bool
Watch bool
Client *fibs.Client
runeBuffer []rune
inputBuffer string
Debug int
}
func NewGame() *Game {
go func() {
// TODO fetch HTTP request, set debugExtra
}()
g := &Game{
op: &ebiten.DrawImageOptions{
Filter: ebiten.FilterNearest,
},
Board: NewBoard(),
runeBuffer: make([]rune, 24),
}
go func() {
t := time.NewTicker(time.Second / 4)
for range t.C {
_ = g.update()
}
}()
return g
}
func (g *Game) handleEvents() {
for e := range g.Client.Event {
switch event := e.(type) {
case *fibs.EventMove:
g.Board.movePiece(event.From, event.To)
}
}
}
func (g *Game) Connect() {
g.loggedIn = true
address := g.ServerAddress
if address == "" {
address = defaultServerAddress
}
g.Client = fibs.NewClient(address, g.Username, g.Password)
go g.handleEvents()
c := g.Client
if g.Watch {
go func() {
time.Sleep(1 * time.Second)
c.WatchRandomGame()
go g.renderLoop()
}()
}
go func() {
err := c.Connect()
if err != nil {
log.Fatal(err)
}
}()
}
func (g *Game) renderLoop() {
t := time.NewTicker(time.Second)
for range t.C {
gameBoard := g.Board
v := g.Client.Board.GetIntState()
gameBoard.SetState(v)
}
}
func (g *Game) leftTouched() bool {
for _, id := range g.touchIDs {
x, _ := ebiten.TouchPosition(id)
/*if x < screenWidth/2 {
return true
}*/
_ = x
}
return false
}
func (g *Game) rightTouched() bool {
for _, id := range g.touchIDs {
x, _ := ebiten.TouchPosition(id)
/*if x >= screenWidth/2 {
return true
}*/
_ = x
}
return false
}
// Separate update function for all normal update logic, as Update may only be
// called when there is user input when vsync is disabled.
func (g *Game) update() error {
return nil
}
func (g *Game) Update() error { // Called by ebiten only when input occurs
err := g.update()
if err != nil {
return err
}
if !g.loggedIn {
f := func() {
var clearBuffer bool
defer func() {
if clearBuffer {
g.inputBuffer = ""
if !g.usernameConfirmed {
g.usernameConfirmed = true
} else if g.Password != "" {
g.Connect()
}
}
}()
if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) && len(g.inputBuffer) > 0 {
g.inputBuffer = g.inputBuffer[:len(g.inputBuffer)-1]
}
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
clearBuffer = true
}
g.runeBuffer = ebiten.AppendInputChars(g.runeBuffer[:0])
if len(g.runeBuffer) > 0 {
g.inputBuffer += string(g.runeBuffer)
if strings.ContainsRune(g.inputBuffer, '\n') {
g.inputBuffer = strings.Split(g.inputBuffer, "\n")[0]
clearBuffer = true
}
if !g.usernameConfirmed {
g.Username = g.inputBuffer
} else {
g.Password = g.inputBuffer
}
log.Println("INPUT BUFFER IS:" + g.inputBuffer)
}
}
f()
}
g.touchIDs = ebiten.AppendTouchIDs(g.touchIDs[:0])
// Decrease the number of the sprites.
if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) || g.leftTouched() {
g.sprites.num -= 20
if g.sprites.num < MinSprites {
g.sprites.num = MinSprites
}
}
// Increase the number of the sprites.
if ebiten.IsKeyPressed(ebiten.KeyArrowRight) || g.rightTouched() {
g.sprites.num += 20
if MaxSprites < g.sprites.num {
g.sprites.num = MaxSprites
}
}
if inpututil.IsKeyJustPressed(ebiten.KeyD) {
g.Debug++
if g.Debug == 3 {
g.Debug = 0
}
g.Board.debug = g.Debug
}
g.Board.update()
//g.sprites.Update()
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
screen.Fill(color.RGBA{0, 102, 51, 255})
// Log in screen
if !g.loggedIn {
const welcomeText = `Please enter your FIBS username and password.
If you do not have a FIBS account yet, visit
http://www.fibs.com/help.html#register`
debugBox := image.NewRGBA(image.Rect(0, 0, g.screenW, g.screenH))
debugImg := ebiten.NewImageFromImage(debugBox)
if !g.usernameConfirmed {
ebitenutil.DebugPrint(debugImg, welcomeText+fmt.Sprintf("\n\nUsername: %s", g.Username))
} else {
ebitenutil.DebugPrint(debugImg, welcomeText+fmt.Sprintf("\n\nPassword: %s", strings.Repeat("*", len(g.Password))))
}
g.resetImageOptions()
g.op.GeoM.Scale(2, 2)
screen.DrawImage(debugImg, g.op)
return
}
// Game screen
g.Board.draw(screen)
if g.Debug == 1 {
debugBox := image.NewRGBA(image.Rect(10, 20, 200, 200))
debugImg := ebiten.NewImageFromImage(debugBox)
g.drawBuffer.Reset()
g.drawBuffer.Write([]byte(fmt.Sprintf("FPS %0.0f\nTPS %0.0f", ebiten.CurrentFPS(), ebiten.CurrentTPS())))
/* TODO enable when vsync is able to be turned off
g.spinnerIndex++
if g.spinnerIndex == 4 {
g.spinnerIndex = 0
}*/
scaleFactor := ebiten.DeviceScaleFactor()
if scaleFactor != 1.0 {
g.drawBuffer.WriteRune('\n')
g.drawBuffer.Write([]byte(fmt.Sprintf("SCA %0.1f", scaleFactor)))
}
if debugExtra != nil {
g.drawBuffer.WriteRune('\n')
g.drawBuffer.Write(debugExtra)
}
ebitenutil.DebugPrint(debugImg, g.drawBuffer.String())
g.resetImageOptions()
g.op.GeoM.Translate(3, 0)
g.op.GeoM.Scale(2, 2)
screen.DrawImage(debugImg, g.op)
}
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
s := ebiten.DeviceScaleFactor()
outsideWidth, outsideHeight = int(float64(outsideWidth)*s), int(float64(outsideHeight)*s)
if g.screenW == outsideWidth && g.screenH == outsideHeight {
return outsideWidth, outsideHeight
}
g.screenW, g.screenH = outsideWidth, outsideHeight
g.Board.setRect(0, 0, g.screenW, g.screenH)
return outsideWidth, outsideHeight
}
func (g *Game) resetImageOptions() {
g.op.GeoM.Reset()
}