Position checkers around board

This commit is contained in:
Trevor Slocum 2021-08-19 20:18:28 -07:00
parent 50646f451a
commit 6966d690d2
5 changed files with 221 additions and 63 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
.idea
*.sh
*.wasm

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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