From 6966d690d2e4aab42c187e7292ecdffb4968a13c Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Thu, 19 Aug 2021 20:18:28 -0700 Subject: [PATCH] Position checkers around board --- .gitignore | 2 + README.md | 26 ++++++- game/board.go | 198 +++++++++++++++++++++++++++++++++++++++----------- game/game.go | 56 +++++++++----- main.go | 2 +- 5 files changed, 221 insertions(+), 63 deletions(-) diff --git a/.gitignore b/.gitignore index 485dee6..5dd9ea3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .idea +*.sh +*.wasm diff --git a/README.md b/README.md index da1c316..773f554 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,34 @@ Online backgammon client ([FIBS](http://fibs.com)) **Note:** This application is in pre-alpha state. Here be dragons. +## Play + +There are several ways to play Boxcars. + +### Browser (recommended) + +Visit https://boxcars.rocketnine.space + +### Desktop + +**Note:** You will need to install the dependencies listed for [your platform](https://github.com/hajimehoshi/ebiten/blob/main/README.md#platforms). + +Run the following command to build a `boxcars` executable: + +`go install code.rocketnine.space/tslocum/boxcars@latest` + +Run `~/go/bin/boxcars` to play. + +## Android + +*Coming soon* + ## Support Please share issues and suggestions [here](https://code.rocketnine.space/tslocum/boxcars/issues). ## Dependencies -- [ebiten](github.com/hajimehoshi/ebiten) - 2D game engine +- [ebiten](https://github.com/hajimehoshi/ebiten) - 2D game engine +- [draw2d](https://github.com/llgcode/draw2d) - 2D shape drawing +- [resize](https://github.com/nfnt/resize) - Image resizing diff --git a/game/board.go b/game/board.go index eb6f81e..f16faff 100644 --- a/game/board.go +++ b/game/board.go @@ -3,6 +3,7 @@ package game import ( "image" "image/color" + "log" "math/rand" "github.com/llgcode/draw2d/draw2dimg" @@ -13,31 +14,46 @@ import ( ) type board struct { - Sprites Sprites + Sprites *Sprites op *ebiten.DrawImageOptions backgroundImage *ebiten.Image dragging *Sprite + dragTouchId ebiten.TouchID + + touchIDs []ebiten.TouchID + x, y int w, h int + + spaceWidth int // spaceWidth is also the width and height of checkers + barWidth int + triangleOffset float64 + horizontalBorderSize int + verticalBorderSize int + overlapSize int } func NewBoard() *board { - b := &board{} + b := &board{ + barWidth: 100, + triangleOffset: float64(50), + horizontalBorderSize: 50, + verticalBorderSize: 25, + overlapSize: 97, + Sprites: &Sprites{ + sprites: make([]*Sprite, 24), + num: 24, + }, + } - b.Sprites.sprites = make([]*Sprite, 30) - b.Sprites.num = 30 for i := range b.Sprites.sprites { s := &Sprite{} r := rand.Intn(2) - if r != 1 { - s.image = imgCheckerWhite - } else { - s.image = imgCheckerBlack - } + s.colorWhite = r != 1 s.w, s.h = imgCheckerWhite.Size() @@ -46,44 +62,79 @@ func NewBoard() *board { b.op = &ebiten.DrawImageOptions{} + b.dragTouchId = -1 + return b } +// relX, relY +func (b *board) spacePosition(index int) (int, int) { + log.Printf("%d", index) + if index <= 12 { + return b.spaceWidth * (index - 1), 0 + } + // TODO add innerW innerH + return b.spaceWidth * (index - 13), (b.h - (b.verticalBorderSize)*2) - b.spaceWidth +} + func (b *board) updateBackgroundImage() { + // TODO percentage of screen instead + + borderColor := color.RGBA{65, 40, 14, 255} + + // Border box := image.NewRGBA(image.Rect(0, 0, b.w, b.h)) - img := ebiten.NewImageFromImage(box) - img.Fill(color.RGBA{0, 0, 0, 255}) - + img.Fill(borderColor) b.backgroundImage = ebiten.NewImageFromImage(img) - box = image.NewRGBA(image.Rect(0, 0, b.w-10, b.h-10)) - + // Face + box = image.NewRGBA(image.Rect(0, 0, b.w-(b.horizontalBorderSize*2), b.h-(b.verticalBorderSize*2))) img = ebiten.NewImageFromImage(box) - img.Fill(color.RGBA{101, 56, 24, 255}) - + img.Fill(color.RGBA{120, 63, 25, 255}) b.op.GeoM.Reset() - b.op.GeoM.Translate(float64(5), float64(5)) + b.op.GeoM.Translate(float64(b.horizontalBorderSize), float64(b.verticalBorderSize)) b.backgroundImage.DrawImage(img, b.op) - baseImg := image.NewRGBA(image.Rect(0, 0, b.w-10, b.h-10)) - + baseImg := image.NewRGBA(image.Rect(0, 0, b.w-(b.horizontalBorderSize*2), b.h-(b.verticalBorderSize*2))) gc := draw2dimg.NewGraphicContext(baseImg) - // Set some properties - gc.SetFillColor(color.RGBA{0, 0, 0, 255}) + + // Bar + box = image.NewRGBA(image.Rect(0, 0, b.barWidth, b.h)) + img = ebiten.NewImageFromImage(box) + img.Fill(borderColor) + b.op.GeoM.Reset() + b.op.GeoM.Translate(float64((b.w/2)-(b.barWidth/2)), 0) + b.backgroundImage.DrawImage(img, b.op) // Draw triangles for i := 0; i < 2; i++ { triangleTip := float64(b.h / 2) if i == 0 { - triangleTip -= 50 + triangleTip -= b.triangleOffset } else { - triangleTip += 50 + triangleTip += b.triangleOffset } for j := 0; j < 12; j++ { - gc.MoveTo(float64(100*j), float64(b.h*i)) - gc.LineTo(float64(100*j)+50, triangleTip) - gc.LineTo(float64(100*j)+100, float64(b.h*i)) + colorA := j%2 == 0 + if i == 1 { + colorA = !colorA + } + + if colorA { + gc.SetFillColor(color.RGBA{219.0, 185, 113, 255}) + } else { + gc.SetFillColor(color.RGBA{120.0, 17.0, 0, 255}) + } + + tx := b.spaceWidth * j + ty := b.h * i + if j >= 6 { + tx += b.barWidth + } + gc.MoveTo(float64(tx), float64(ty)) + gc.LineTo(float64(tx+b.spaceWidth/2), triangleTip) + gc.LineTo(float64(tx+b.spaceWidth), float64(ty)) gc.Close() gc.Fill() } @@ -92,7 +143,7 @@ func (b *board) updateBackgroundImage() { img = ebiten.NewImageFromImage(baseImg) b.op.GeoM.Reset() - b.op.GeoM.Translate(float64(5), float64(5)) + b.op.GeoM.Translate(float64(b.horizontalBorderSize), float64(b.verticalBorderSize)) b.backgroundImage.DrawImage(img, b.op) } @@ -105,7 +156,12 @@ func (b *board) draw(screen *ebiten.Image) { sprite := b.Sprites.sprites[i] b.op.GeoM.Reset() b.op.GeoM.Translate(float64(sprite.x), float64(sprite.y)) - screen.DrawImage(sprite.image, b.op) + + if sprite.colorWhite { + screen.DrawImage(imgCheckerWhite, b.op) + } else { + screen.DrawImage(imgCheckerBlack, b.op) + } } } @@ -116,48 +172,102 @@ func (b *board) setRect(x, y, w, h int) { b.x, b.y, b.w, b.h = x, y, w, h + b.spaceWidth = ((b.w - (b.horizontalBorderSize * 2)) - b.barWidth) / 12 + + loadAssets(b.spaceWidth) + for i := 0; i < b.Sprites.num; i++ { + s := b.Sprites.sprites[i] + log.Printf("%d-%d", s.w, s.h) + s.w, s.h = imgCheckerWhite.Size() + log.Printf("NEW %d-%d", s.w, s.h) + } + b.updateBackgroundImage() b.positionCheckers() } func (b *board) offsetPosition(x, y int) (int, int) { - const boardPadding = 7 - return b.x + x + boardPadding, b.y + y + boardPadding + return b.x + x + b.horizontalBorderSize, b.y + y + b.verticalBorderSize } func (b *board) positionCheckers() { + // TODO slightly overlap to save space for i := 0; i < b.Sprites.num; i++ { s := b.Sprites.sprites[i] - s.x, s.y = b.offsetPosition(s.w*(i%12), s.h*(i/12)) + if b.dragging == s { + continue + } + + spaceIndex := i + 1 + + x, y := b.spacePosition(spaceIndex) + s.x, s.y = b.offsetPosition(x, y) + if (spaceIndex > 6 && spaceIndex < 13) || (spaceIndex > 18 && spaceIndex < 25) { + s.x += b.barWidth + } + s.x += (b.spaceWidth - s.w) / 2 + + /* multiple pieces + if i <= 12 { + s.y += b.overlapSize + } else { + s.y -= b.overlapSize + }*/ } } +func (b *board) spriteAt(x, y int) *Sprite { + for i := 0; i < b.Sprites.num; i++ { + s := b.Sprites.sprites[i] + if x >= s.x && y >= s.y && x <= s.x+s.w && y <= s.y+s.h { + // Bring sprite to front + b.Sprites.sprites = append(b.Sprites.sprites[:i], b.Sprites.sprites[i+1:]...) + b.Sprites.sprites = append(b.Sprites.sprites, s) + + return s + } + } + return nil +} + func (b *board) update() { - if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { - x, y := ebiten.CursorPosition() + if b.dragging == nil { + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { + x, y := ebiten.CursorPosition() - if b.dragging == nil { - for i := 0; i < b.Sprites.num; i++ { - s := b.Sprites.sprites[i] - if x >= s.x && y >= s.y && x <= s.x+s.w && y <= s.y+s.h { + if b.dragging == nil { + s := b.spriteAt(x, y) + if s != nil { b.dragging = s - - // Bring sprite to front - b.Sprites.sprites = append(b.Sprites.sprites[:i], b.Sprites.sprites[i+1:]...) - b.Sprites.sprites = append(b.Sprites.sprites, s) - - break } } } + + b.touchIDs = inpututil.AppendJustPressedTouchIDs(b.touchIDs[:0]) + for _, id := range b.touchIDs { + x, y := ebiten.TouchPosition(id) + s := b.spriteAt(x, y) + if s != nil { + b.dragging = s + b.dragTouchId = id + } + } } - if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) { + if b.dragTouchId == -1 { + if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) { + b.dragging = nil + } + } else if inpututil.IsTouchJustReleased(b.dragTouchId) { b.dragging = nil } if b.dragging != nil { x, y := ebiten.CursorPosition() + if b.dragTouchId != -1 { + x, y = ebiten.TouchPosition(b.dragTouchId) + } + sprite := b.dragging sprite.x = x - (sprite.w / 2) sprite.y = y - (sprite.h / 2) diff --git a/game/game.go b/game/game.go index 75943e6..9f1cba9 100644 --- a/game/game.go +++ b/game/game.go @@ -1,6 +1,7 @@ package game import ( + "bytes" "embed" "fmt" "image" @@ -53,13 +54,17 @@ var ( ) func init() { - imgCheckerWhite = loadAsset("assets/checker_white.png") - imgCheckerBlack = loadAsset("assets/checker_black.png") + loadAssets(0) initializeFonts() } -func loadAsset(assetPath string) *ebiten.Image { +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) @@ -70,9 +75,11 @@ func loadAsset(assetPath string) *ebiten.Image { log.Fatal(err) } - imgResized := resize.Resize(100, 0, img, resize.Lanczos3) - - return ebiten.NewImageFromImage(imgResized) + if width > 0 { + imgResized := resize.Resize(uint(width), 0, img, resize.Lanczos3) + return ebiten.NewImageFromImage(imgResized) + } + return ebiten.NewImageFromImage(img) } @@ -161,14 +168,12 @@ func line(x0, y0, x1, y1 float32, clr color.RGBA) ([]ebiten.Vertex, []uint16) { } type Sprite struct { - image *ebiten.Image - w int - h int - x int - y int - vx int - vy int - angle int + image *ebiten.Image + w int + h int + x int + y int + colorWhite bool } func (s *Sprite) Update() { @@ -198,6 +203,8 @@ type Game struct { board *board screenW, screenH int + + drawBuffer bytes.Buffer } func NewGame() *Game { @@ -270,8 +277,22 @@ func (g *Game) Draw(screen *ebiten.Image) { debugBox := image.NewRGBA(image.Rect(10, 20, 200, 200)) debugImg := ebiten.NewImageFromImage(debugBox) - msg := fmt.Sprintf("FPS %0.0f\nTPS %0.0f\n%s", ebiten.CurrentTPS(), ebiten.CurrentFPS(), debugExtra) - ebitenutil.DebugPrint(debugImg, msg) + g.drawBuffer.Reset() + + g.drawBuffer.Write([]byte(fmt.Sprintf("FPS %0.0f\nTPS %0.0f", ebiten.CurrentFPS(), ebiten.CurrentTPS()))) + + 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) @@ -286,8 +307,9 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { g.screenW, g.screenH = outsideWidth, outsideHeight - g.board.setRect(300, 25, g.screenW-325, g.screenH-50) + g.board.setRect(300, 0, g.screenW-300, g.screenH) + // TODO use scale factor return outsideWidth, outsideHeight } diff --git a/main.go b/main.go index 0bb5e53..f2f9965 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ func main() { ebiten.SetWindowTitle("Boxcars") ebiten.SetWindowSize(screenWidth, screenHeight) ebiten.SetWindowResizable(true) - ebiten.SetMaxTPS(144) // TODO tune + ebiten.SetMaxTPS(60) // TODO allow users to set custom value ebiten.SetRunnableOnUnfocused(true) // Note - this currently does nothing in ebiten //ebiten.SetWindowClosingHandled(true) TODO implement