feat: track players' score

This commit is contained in:
Dawid Wysokiński 2023-11-30 07:28:09 +01:00
parent fb59445434
commit 54324f9153
Signed by: Kichiyaki
GPG Key ID: B5445E357FB8B892
11 changed files with 259 additions and 62 deletions

7
go.mod
View File

@ -2,14 +2,17 @@ module gitea.dwysokinski.me/classic-games/tennis-game
go 1.21
require github.com/hajimehoshi/ebiten/v2 v2.6.3
require (
github.com/hajimehoshi/ebiten/v2 v2.6.3
golang.org/x/image v0.12.0
)
require (
github.com/ebitengine/purego v0.5.0 // indirect
github.com/jezek/xgb v1.1.0 // indirect
golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63 // indirect
golang.org/x/image v0.12.0 // indirect
golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
)

3
go.sum
View File

@ -1,5 +1,7 @@
github.com/ebitengine/purego v0.5.0 h1:JrMGKfRIAM4/QVKaesIIT7m/UVjTj5GYhRSQYwfVdpo=
github.com/ebitengine/purego v0.5.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/hajimehoshi/bitmapfont/v3 v3.0.0 h1:r2+6gYK38nfztS/et50gHAswb9hXgxXECYgE8Nczmi4=
github.com/hajimehoshi/bitmapfont/v3 v3.0.0/go.mod h1:+CxxG+uMmgU4mI2poq944i3uZ6UYFfAkj9V6WqmuvZA=
github.com/hajimehoshi/ebiten/v2 v2.6.3 h1:xJ5klESxhflZbPUx3GdIPoITzgPgamsyv8aZCVguXGI=
github.com/hajimehoshi/ebiten/v2 v2.6.3/go.mod h1:TZtorL713an00UW4LyvMeKD8uXWnuIuCPtlH11b0pgI=
github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk=
@ -39,6 +41,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=

View File

@ -10,8 +10,10 @@ import (
type ball struct {
x float32
speedX float32
y float32
initX float32
initY float32
speedX float32
speedY float32
r float32
}
@ -26,10 +28,14 @@ func newBall(screenWidth, screenHeight int) *ball {
fScreenWidth := float32(screenWidth)
fScreenHeight := float32(screenHeight)
r := ballBaseRadius * fScreenHeight / BaseHeight
x := fScreenWidth/2.0 - r
y := fScreenHeight/2.0 - r
return &ball{
x: fScreenWidth/2.0 - r,
x: x,
y: y,
initX: x,
initY: y,
speedX: ballBaseSpeedX * fScreenWidth / BaseWidth,
y: fScreenHeight/2.0 - r,
speedY: ballBaseSpeedY * fScreenHeight / BaseHeight,
r: r,
}
@ -73,10 +79,25 @@ func (b *ball) nextSpeed(bounds image.Rectangle) (speedX, speedY float32) {
return speedX, speedY
}
func (b *ball) reset() {
b.x = b.initX
b.y = b.initY
}
func (b *ball) resize(x, y float32) {
b.x *= x
b.speedX *= x
b.initX *= x
b.y *= y
b.speedY *= y
b.initY *= y
b.r *= y
}
func (b *ball) reverseDirectionHorizontal() {
b.speedX *= -1
}
func (b *ball) bounds() image.Rectangle {
return image.Rect(int(b.x-b.r), int(b.y-b.r), int(b.x+b.r), int(b.y+b.r))
}

70
internal/court.go Normal file
View File

@ -0,0 +1,70 @@
package internal
import (
"fmt"
"image"
"image/color"
"github.com/hajimehoshi/ebiten/v2"
)
// match represents the game match
type match struct {
ball *ball
leftPaddle *paddle
rightPaddle *paddle
score *score
}
func newMatch(screenWidth, screenHeight int, f *fonts) *match {
return &match{
ball: newBall(screenWidth, screenHeight),
leftPaddle: newLeftPaddle(screenWidth, screenHeight, true),
rightPaddle: newRightPaddle(screenWidth, screenHeight, false),
score: &score{
fonts: f,
},
}
}
var backgroundColor = color.Black
func (m *match) draw(img *ebiten.Image) {
img.Fill(backgroundColor)
m.ball.draw(img)
m.leftPaddle.draw(img)
m.rightPaddle.draw(img)
m.score.draw(img)
}
func (m *match) update(bounds image.Rectangle) error {
if err := m.ball.update(bounds); err != nil {
return fmt.Errorf("couldn't update ball: %w", err)
}
if err := m.leftPaddle.update(bounds, m.ball); err != nil {
return fmt.Errorf("couldn't update left paddle: %w", err)
}
if err := m.rightPaddle.update(bounds, m.ball); err != nil {
return fmt.Errorf("couldn't update right paddle: %w", err)
}
if m.ball.bounds().Intersect(m.leftPaddle.bounds()) != (image.Rectangle{}) ||
m.ball.bounds().Intersect(m.rightPaddle.bounds()) != (image.Rectangle{}) {
m.ball.reverseDirectionHorizontal()
}
if err := m.score.update(bounds, m.ball, m.leftPaddle, m.rightPaddle); err != nil {
return fmt.Errorf("couldn't update score: %w", err)
}
return nil
}
func (m *match) resize(x, y float32) {
m.ball.resize(x, y)
m.leftPaddle.resize(x, y)
m.rightPaddle.resize(x, y)
}

33
internal/fonts.go Normal file
View File

@ -0,0 +1,33 @@
package internal
import (
"fmt"
"gitea.dwysokinski.me/classic-games/tennis-game/internal/resource"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
)
type fonts struct {
robotoRegularFont font.Face
}
func newFonts() (*fonts, error) {
robotoRegular, err := opentype.Parse(resource.FontRobotoRegular)
if err != nil {
return nil, fmt.Errorf("couldn't parse roboto regular: %w", err)
}
robotoRegularFont, err := opentype.NewFace(robotoRegular, &opentype.FaceOptions{
Size: 24,
DPI: 72,
Hinting: font.HintingVertical,
})
if err != nil {
return nil, fmt.Errorf("couldn't construct roboto regular font.Face: %w", err)
}
return &fonts{
robotoRegularFont: robotoRegularFont,
}, nil
}

View File

@ -3,7 +3,6 @@ package internal
import (
"fmt"
"image"
"image/color"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
@ -15,58 +14,45 @@ const (
)
type Game struct {
width int
height int
ball *ball
leftPaddle *paddle
rightPaddle *paddle
debug bool
img *ebiten.Image
width int
height int
match *match
debug bool
img *ebiten.Image
}
var _ ebiten.Game = (*Game)(nil)
func NewGame(width, height int, debug bool) *Game {
return &Game{
width: width,
height: height,
ball: newBall(width, height),
leftPaddle: newLeftPaddle(width, height, true),
rightPaddle: newRightPaddle(width, height, false),
debug: debug,
func NewGame(width, height int, debug bool) (*Game, error) {
f, err := newFonts()
if err != nil {
return nil, fmt.Errorf("couldn't parse fonts: %w", err)
}
return &Game{
width: width,
height: height,
match: newMatch(width, height, f),
debug: debug,
}, nil
}
func (g *Game) Update() error {
bounds := image.Rect(0, 0, g.width, g.height)
if err := g.ball.update(bounds); err != nil {
return fmt.Errorf("couldn't update ball: %w", err)
}
if err := g.leftPaddle.update(bounds, g.ball); err != nil {
return fmt.Errorf("couldn't update left paddle: %w", err)
}
if err := g.rightPaddle.update(bounds, g.ball); err != nil {
return fmt.Errorf("couldn't update right paddle: %w", err)
if err := g.match.update(bounds); err != nil {
return fmt.Errorf("couldn't update match: %w", err)
}
return nil
}
var backgroundColor = color.Black
func (g *Game) Draw(screen *ebiten.Image) {
if g.img == nil {
g.img = ebiten.NewImage(g.width, g.height)
}
g.img.Fill(backgroundColor)
g.ball.draw(g.img)
g.leftPaddle.draw(g.img)
g.rightPaddle.draw(g.img)
g.match.draw(g.img)
if g.debug {
g.drawDebug()
@ -84,23 +70,23 @@ func (g *Game) drawDebug() {
"\nLeft paddle: x=%v, y=%v, speed=%v, width=%v, height=%v, isControlledByPlayer=%v"+
"\nRight paddle: x=%v, y=%v, speed=%v, width=%v, height=%v, isControlledByPlayer=%v",
ebiten.ActualFPS(),
g.ball.x,
g.ball.y,
g.ball.speedX,
g.ball.speedY,
g.ball.r,
g.leftPaddle.x,
g.leftPaddle.y,
g.leftPaddle.speed,
g.leftPaddle.width,
g.leftPaddle.height,
g.leftPaddle.isControlledByPlayer,
g.rightPaddle.x,
g.rightPaddle.y,
g.rightPaddle.speed,
g.rightPaddle.width,
g.rightPaddle.height,
g.rightPaddle.isControlledByPlayer,
g.match.ball.x,
g.match.ball.y,
g.match.ball.speedX,
g.match.ball.speedY,
g.match.ball.r,
g.match.leftPaddle.x,
g.match.leftPaddle.y,
g.match.leftPaddle.speed,
g.match.leftPaddle.width,
g.match.leftPaddle.height,
g.match.leftPaddle.isControlledByPlayer,
g.match.rightPaddle.x,
g.match.rightPaddle.y,
g.match.rightPaddle.speed,
g.match.rightPaddle.width,
g.match.rightPaddle.height,
g.match.rightPaddle.isControlledByPlayer,
),
)
}
@ -108,12 +94,7 @@ func (g *Game) drawDebug() {
//nolint:nonamedreturns
func (g *Game) Layout(outsideWidth, outsideHeight int) (_screenWidth, _screenHeight int) {
if g.width != outsideWidth || g.height != outsideHeight {
ratioWidth := float32(outsideWidth) / float32(g.width)
ratioHeight := float32(outsideHeight) / float32(g.height)
g.ball.resize(ratioWidth, ratioHeight)
g.leftPaddle.resize(ratioWidth, ratioHeight)
g.rightPaddle.resize(ratioWidth, ratioHeight)
g.match.resize(float32(outsideWidth)/float32(g.width), float32(outsideHeight)/float32(g.height))
if g.img != nil {
g.img.Dispose()

View File

@ -114,6 +114,11 @@ func (p *paddle) update(bounds image.Rectangle, b *ball) error {
return nil
}
func (p *paddle) reset() {
p.x = p.initX
p.y = p.initY
}
func (p *paddle) resize(x, y float32) {
p.x *= x
p.width *= x
@ -123,3 +128,7 @@ func (p *paddle) resize(x, y float32) {
p.speed *= y
p.initY *= y
}
func (p *paddle) bounds() image.Rectangle {
return image.Rect(int(p.x), int(p.y), int(p.x+p.width), int(p.y+p.height))
}

Binary file not shown.

View File

@ -0,0 +1,10 @@
package resource
import (
_ "embed"
)
var (
//go:embed Roboto-Regular.ttf
FontRobotoRegular []byte
)

62
internal/score.go Normal file
View File

@ -0,0 +1,62 @@
package internal
import (
"image"
"image/color"
"strconv"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/text"
)
type score struct {
left int
right int
fonts *fonts
}
var scoreTextColor = color.White
func (s *score) draw(img *ebiten.Image) {
bounds := img.Bounds()
y := bounds.Max.Y / 4
text.Draw(img, strconv.Itoa(s.left), s.fonts.robotoRegularFont, bounds.Max.X/4, y, scoreTextColor)
text.Draw(img, strconv.Itoa(s.right), s.fonts.robotoRegularFont, bounds.Max.X*3/4, y, scoreTextColor)
}
func (s *score) update(
bounds image.Rectangle,
b *ball,
leftPaddle *paddle,
rightPaddle *paddle,
) error {
ballBounds := b.bounds()
if ballBounds.
Intersect(image.Rect(
int(leftPaddle.x),
bounds.Min.Y,
int(leftPaddle.width/2),
bounds.Max.Y,
)) != (image.Rectangle{}) {
s.right++
b.reset()
leftPaddle.reset()
rightPaddle.reset()
}
if ballBounds.
Intersect(image.Rect(
int(rightPaddle.x+rightPaddle.width/2),
bounds.Min.Y,
bounds.Max.X,
bounds.Max.Y,
)) != (image.Rectangle{}) {
s.left++
b.reset()
leftPaddle.reset()
rightPaddle.reset()
}
return nil
}

View File

@ -24,7 +24,12 @@ func main() {
ebiten.SetWindowSize(w, h)
}
if err := ebiten.RunGame(internal.NewGame(w, h, *debug)); err != nil {
game, err := internal.NewGame(w, h, *debug)
if err != nil {
log.Fatal(err)
}
if err = ebiten.RunGame(game); err != nil {
log.Fatal(err)
}
}