ebiten/examples/2048/2048/tile.go

405 lines
9.0 KiB
Go
Raw Normal View History

2016-07-27 21:30:10 +02:00
// Copyright 2016 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.
package twenty48
2016-07-30 10:23:39 +02:00
import (
"errors"
2016-07-30 10:54:05 +02:00
"image/color"
2018-01-19 04:07:34 +01:00
"log"
2016-07-30 10:23:39 +02:00
"math/rand"
"sort"
2016-07-30 10:54:05 +02:00
"strconv"
2018-01-19 04:07:34 +01:00
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
2018-01-19 04:07:34 +01:00
2020-10-03 19:35:13 +02:00
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
"github.com/hajimehoshi/ebiten/v2/text"
2018-01-19 04:07:34 +01:00
)
var (
2018-01-19 04:12:29 +01:00
mplusSmallFont font.Face
2018-01-19 04:07:34 +01:00
mplusNormalFont font.Face
mplusBigFont font.Face
2016-07-30 10:23:39 +02:00
)
2018-01-19 04:07:34 +01:00
func init() {
tt, err := opentype.Parse(fonts.MPlus1pRegular_ttf)
2018-01-19 04:07:34 +01:00
if err != nil {
log.Fatal(err)
}
const dpi = 72
mplusSmallFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
2018-01-19 04:12:29 +01:00
Size: 24,
DPI: dpi,
Hinting: font.HintingFull,
})
if err != nil {
log.Fatal(err)
}
mplusNormalFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
2018-01-19 04:12:29 +01:00
Size: 32,
2018-01-19 04:07:34 +01:00
DPI: dpi,
Hinting: font.HintingFull,
})
if err != nil {
log.Fatal(err)
}
mplusBigFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
2018-01-19 04:12:29 +01:00
Size: 48,
2018-01-19 04:07:34 +01:00
DPI: dpi,
Hinting: font.HintingFull,
})
if err != nil {
log.Fatal(err)
}
2018-01-19 04:07:34 +01:00
}
2017-06-04 09:04:51 +02:00
// TileData represents a tile information like a value and a position.
2016-07-30 10:54:05 +02:00
type TileData struct {
2016-07-27 21:30:10 +02:00
value int
x int
y int
}
2017-07-23 15:49:04 +02:00
// Tile represents a tile information including TileData and animation states.
2016-07-30 10:54:05 +02:00
type Tile struct {
2017-06-04 11:42:35 +02:00
current TileData
// next represents a next tile information after moving.
// next is empty when the tile is not about to move.
next TileData
2016-07-31 13:56:46 +02:00
movingCount int
startPoppingCount int
poppingCount int
2016-07-30 10:54:05 +02:00
}
// Pos returns the tile's current position.
// Pos is used only at testing so far.
func (t *Tile) Pos() (int, int) {
return t.current.x, t.current.y
}
// NextPos returns the tile's next position.
// NextPos is used only at testing so far.
func (t *Tile) NextPos() (int, int) {
return t.next.x, t.next.y
}
// Value returns the tile's current value.
// Value is used only at testing so far.
func (t *Tile) Value() int {
return t.current.value
}
// NextValue returns the tile's current value.
// NextValue is used only at testing so far.
func (t *Tile) NextValue() int {
return t.next.value
}
2017-06-04 09:04:51 +02:00
// NewTile creates a new Tile object.
2016-07-27 21:30:10 +02:00
func NewTile(value int, x, y int) *Tile {
return &Tile{
2016-07-30 10:54:05 +02:00
current: TileData{
value: value,
x: x,
y: y,
},
2016-07-31 13:56:46 +02:00
startPoppingCount: maxPoppingCount,
2016-07-27 21:30:10 +02:00
}
}
2016-07-28 17:00:12 +02:00
2017-06-04 09:04:51 +02:00
// IsMoving returns a boolean value indicating if the tile is animating.
2016-07-31 10:25:39 +02:00
func (t *Tile) IsMoving() bool {
2016-07-31 13:56:46 +02:00
return 0 < t.movingCount
2016-07-31 10:25:39 +02:00
}
2016-07-31 19:42:06 +02:00
func (t *Tile) stopAnimation() {
if 0 < t.movingCount {
t.current = t.next
t.next = TileData{}
}
2016-07-31 19:42:06 +02:00
t.movingCount = 0
t.startPoppingCount = 0
t.poppingCount = 0
2016-07-30 19:49:28 +02:00
}
2016-07-30 10:23:39 +02:00
func tileAt(tiles map[*Tile]struct{}, x, y int) *Tile {
2016-07-30 17:55:29 +02:00
var result *Tile
2016-07-30 10:23:39 +02:00
for t := range tiles {
2016-07-30 17:55:29 +02:00
if t.current.x != x || t.current.y != y {
continue
}
if result != nil {
panic("not reach")
2016-07-30 10:23:39 +02:00
}
2016-07-30 17:55:29 +02:00
result = t
2016-07-30 10:23:39 +02:00
}
2016-07-30 17:55:29 +02:00
return result
2016-07-30 10:23:39 +02:00
}
2017-06-04 11:42:35 +02:00
func currentOrNextTileAt(tiles map[*Tile]struct{}, x, y int) *Tile {
2016-07-30 17:55:29 +02:00
var result *Tile
for t := range tiles {
2016-07-31 13:56:46 +02:00
if 0 < t.movingCount {
2016-07-31 07:57:42 +02:00
if t.next.x != x || t.next.y != y || t.next.value == 0 {
continue
}
} else {
if t.current.x != x || t.current.y != y {
continue
}
2016-07-30 17:55:29 +02:00
}
if result != nil {
panic("not reach")
}
result = t
}
return result
}
2016-07-30 19:49:28 +02:00
const (
2016-07-31 10:25:39 +02:00
maxMovingCount = 5
maxPoppingCount = 6
2016-07-30 19:49:28 +02:00
)
2017-06-04 09:04:51 +02:00
// MoveTiles moves tiles in the given tiles map if possible.
2017-06-04 11:00:47 +02:00
// MoveTiles returns true if there are tiles that are to move, otherwise false.
2017-06-04 11:42:35 +02:00
//
// When MoveTiles is called, all tiles must not be about to move.
2016-07-30 17:55:29 +02:00
func MoveTiles(tiles map[*Tile]struct{}, size int, dir Dir) bool {
2016-07-30 10:23:39 +02:00
vx, vy := dir.Vector()
tx := []int{}
ty := []int{}
for i := 0; i < size; i++ {
tx = append(tx, i)
ty = append(ty, i)
}
if vx > 0 {
sort.Sort(sort.Reverse(sort.IntSlice(tx)))
}
if vy > 0 {
sort.Sort(sort.Reverse(sort.IntSlice(ty)))
}
moved := false
for _, j := range ty {
for _, i := range tx {
t := tileAt(tiles, i, j)
if t == nil {
continue
}
if t.next != (TileData{}) {
panic("not reach")
}
2016-07-31 10:25:39 +02:00
if t.IsMoving() {
2016-07-30 19:49:28 +02:00
panic("not reach")
}
2017-06-04 11:42:35 +02:00
// (ii, jj) is the next position for tile t.
// (ii, jj) is updated until a mergeable tile is found or
// the tile t can't be moved any more.
2016-07-30 10:23:39 +02:00
ii := i
jj := j
for {
ni := ii + vx
nj := jj + vy
if ni < 0 || ni >= size || nj < 0 || nj >= size {
break
}
2017-06-04 11:42:35 +02:00
tt := currentOrNextTileAt(tiles, ni, nj)
2016-07-30 10:23:39 +02:00
if tt == nil {
ii = ni
jj = nj
moved = true
continue
}
2016-07-31 07:57:42 +02:00
if t.current.value != tt.current.value {
2016-07-30 10:23:39 +02:00
break
}
2016-07-31 13:56:46 +02:00
if 0 < tt.movingCount && tt.current.value != tt.next.value {
2017-06-04 11:42:35 +02:00
// tt is already being merged with another tile.
// Break here without updating (ii, jj).
2016-07-30 17:55:29 +02:00
break
2016-07-30 10:23:39 +02:00
}
2016-07-30 17:55:29 +02:00
ii = ni
jj = nj
moved = true
2016-07-30 10:23:39 +02:00
break
}
2017-06-04 11:42:35 +02:00
// next is the next state of the tile t.
2016-07-30 19:49:28 +02:00
next := TileData{}
next.value = t.current.value
2017-06-04 11:42:35 +02:00
// If there is a tile at the next position (ii, jj), this should be
// mergeable. Let's merge.
if tt := currentOrNextTileAt(tiles, ii, jj); tt != t && tt != nil {
2016-07-31 07:57:42 +02:00
next.value = t.current.value + tt.current.value
tt.next.value = 0
2016-07-31 07:57:42 +02:00
tt.next.x = ii
tt.next.y = jj
2016-07-31 13:56:46 +02:00
tt.movingCount = maxMovingCount
2016-07-30 10:23:39 +02:00
}
2016-07-30 19:49:28 +02:00
next.x = ii
next.y = jj
2016-07-31 07:57:42 +02:00
if t.current != next {
t.next = next
2016-07-31 13:56:46 +02:00
t.movingCount = maxMovingCount
2016-07-31 07:57:42 +02:00
}
2016-07-30 10:23:39 +02:00
}
}
if !moved {
for t := range tiles {
t.next = TileData{}
2016-07-31 13:56:46 +02:00
t.movingCount = 0
}
}
2016-07-30 17:55:29 +02:00
return moved
2016-07-30 10:23:39 +02:00
}
func addRandomTile(tiles map[*Tile]struct{}, size int) error {
cells := make([]bool, size*size)
for t := range tiles {
2016-07-31 10:25:39 +02:00
if t.IsMoving() {
2016-07-30 19:49:28 +02:00
panic("not reach")
2016-07-30 17:55:29 +02:00
}
2016-07-30 19:49:28 +02:00
i := t.current.x + t.current.y*size
2016-07-30 10:23:39 +02:00
cells[i] = true
}
availableCells := []int{}
for i, b := range cells {
if b {
continue
}
availableCells = append(availableCells, i)
}
if len(availableCells) == 0 {
return errors.New("twenty48: there is no space to add a new tile")
}
c := availableCells[rand.Intn(len(availableCells))]
v := 2
if rand.Intn(10) == 0 {
v = 4
}
x := c % size
y := c / size
t := NewTile(v, x, y)
tiles[t] = struct{}{}
return nil
}
2016-07-30 10:54:05 +02:00
2017-06-04 11:00:47 +02:00
// Update updates the tile's animation states.
2016-07-30 17:55:29 +02:00
func (t *Tile) Update() error {
2016-07-31 10:25:39 +02:00
switch {
2016-07-31 13:56:46 +02:00
case 0 < t.movingCount:
t.movingCount--
if t.movingCount == 0 {
2016-07-31 10:25:39 +02:00
if t.current.value != t.next.value && 0 < t.next.value {
2016-07-31 13:56:46 +02:00
t.poppingCount = maxPoppingCount
2016-07-31 10:25:39 +02:00
}
t.current = t.next
t.next = TileData{}
2016-07-30 19:49:28 +02:00
}
2016-07-31 13:56:46 +02:00
case 0 < t.startPoppingCount:
t.startPoppingCount--
case 0 < t.poppingCount:
t.poppingCount--
2016-07-30 19:49:28 +02:00
}
2016-07-30 17:55:29 +02:00
return nil
}
2016-07-30 19:49:28 +02:00
func mean(a, b int, rate float64) int {
2016-07-31 10:25:39 +02:00
return int(float64(a)*(1-rate) + float64(b)*rate)
}
func meanF(a, b float64, rate float64) float64 {
return a*(1-rate) + b*rate
2016-07-30 19:49:28 +02:00
}
const (
2018-01-19 04:12:29 +01:00
tileSize = 80
tileMargin = 4
2016-07-30 19:49:28 +02:00
)
var (
tileImage = ebiten.NewImage(tileSize, tileSize)
2016-07-30 19:49:28 +02:00
)
func init() {
2017-06-02 17:51:15 +02:00
tileImage.Fill(color.White)
2016-07-30 19:49:28 +02:00
}
2017-06-04 09:04:51 +02:00
// Draw draws the current tile to the given boardImage.
func (t *Tile) Draw(boardImage *ebiten.Image) {
2016-07-30 10:54:05 +02:00
i, j := t.current.x, t.current.y
2016-07-30 19:49:28 +02:00
ni, nj := t.next.x, t.next.y
2016-07-30 17:55:29 +02:00
v := t.current.value
2016-07-30 19:49:28 +02:00
if v == 0 {
return
2016-07-30 19:49:28 +02:00
}
2016-07-30 10:54:05 +02:00
op := &ebiten.DrawImageOptions{}
x := i*tileSize + (i+1)*tileMargin
y := j*tileSize + (j+1)*tileMargin
2016-07-30 19:49:28 +02:00
nx := ni*tileSize + (ni+1)*tileMargin
ny := nj*tileSize + (nj+1)*tileMargin
2016-07-31 13:56:46 +02:00
switch {
case 0 < t.movingCount:
rate := 1 - float64(t.movingCount)/maxMovingCount
2016-07-30 19:49:28 +02:00
x = mean(x, nx, rate)
y = mean(y, ny, rate)
2016-07-31 13:56:46 +02:00
case 0 < t.startPoppingCount:
rate := 1 - float64(t.startPoppingCount)/float64(maxPoppingCount)
scale := meanF(0.0, 1.0, rate)
op.GeoM.Translate(float64(-tileSize/2), float64(-tileSize/2))
op.GeoM.Scale(scale, scale)
op.GeoM.Translate(float64(tileSize/2), float64(tileSize/2))
case 0 < t.poppingCount:
2016-07-31 10:25:39 +02:00
const maxScale = 1.2
rate := 0.0
2016-07-31 13:56:46 +02:00
if maxPoppingCount*2/3 <= t.poppingCount {
2016-07-31 10:25:39 +02:00
// 0 to 1
2016-07-31 13:56:46 +02:00
rate = 1 - float64(t.poppingCount-2*maxPoppingCount/3)/float64(maxPoppingCount/3)
2016-07-31 10:25:39 +02:00
} else {
// 1 to 0
2016-07-31 13:56:46 +02:00
rate = float64(t.poppingCount) / float64(maxPoppingCount*2/3)
2016-07-31 10:25:39 +02:00
}
scale := meanF(1.0, maxScale, rate)
op.GeoM.Translate(float64(-tileSize/2), float64(-tileSize/2))
op.GeoM.Scale(scale, scale)
op.GeoM.Translate(float64(tileSize/2), float64(tileSize/2))
}
2016-07-30 10:54:05 +02:00
op.GeoM.Translate(float64(x), float64(y))
op.ColorM.ScaleWithColor(tileBackgroundColor(v))
boardImage.DrawImage(tileImage, op)
2016-07-30 10:54:05 +02:00
str := strconv.Itoa(v)
2018-01-19 04:07:34 +01:00
f := mplusBigFont
2018-01-19 04:12:29 +01:00
switch {
case 3 < len(str):
f = mplusSmallFont
case 2 < len(str):
2018-01-19 04:07:34 +01:00
f = mplusNormalFont
2016-07-30 10:54:05 +02:00
}
2018-01-19 04:07:34 +01:00
bound, _ := font.BoundString(f, str)
w := (bound.Max.X - bound.Min.X).Ceil()
h := (bound.Max.Y - bound.Min.Y).Ceil()
2016-07-30 10:54:05 +02:00
x = x + (tileSize-w)/2
2018-01-19 04:07:34 +01:00
y = y + (tileSize-h)/2 + h
text.Draw(boardImage, str, f, x, y, tileColor(v))
2016-07-30 10:54:05 +02:00
}