feat: track players' score
This commit is contained in:
parent
fb59445434
commit
54324f9153
7
go.mod
7
go.mod
|
@ -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
3
go.sum
|
@ -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=
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
@ -0,0 +1,10 @@
|
|||
package resource
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed Roboto-Regular.ttf
|
||||
FontRobotoRegular []byte
|
||||
)
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue