initial
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
package puzzle
|
||||
|
||||
import (
|
||||
"15-puzzle/internal/model"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"math/rand/v2"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
tiles = 16
|
||||
fieldSymX = 29
|
||||
fieldSymY = 12
|
||||
)
|
||||
|
||||
var (
|
||||
checkbox = map[bool]string{true: "[x]", false: "[ ]"}
|
||||
frames = [...]byte{'|', '/', '-', '\\'}
|
||||
)
|
||||
|
||||
type game struct {
|
||||
langCode langCode
|
||||
tiles [tiles]*tile
|
||||
moves int
|
||||
solved bool
|
||||
muted bool
|
||||
onStart func()
|
||||
onSolve func(int)
|
||||
requestStats func()
|
||||
|
||||
stats atomic.Value
|
||||
blinkCoef []float64
|
||||
|
||||
blink [fieldSymX][fieldSymY]int
|
||||
color [fieldSymX][fieldSymY]color.RGBA
|
||||
}
|
||||
|
||||
func newGame(onStart func(), onSolve func(int), request func()) *game {
|
||||
p := &game{
|
||||
langCode: langCodeEn,
|
||||
solved: true,
|
||||
onStart: onStart,
|
||||
onSolve: onSolve,
|
||||
requestStats: request,
|
||||
blinkCoef: []float64{1, .8, .6, .4, .2, 0, 0, .2, .4, .6, .8, 1},
|
||||
}
|
||||
|
||||
for i := range tiles {
|
||||
var pos int
|
||||
if i == 0 {
|
||||
pos = tiles - 1
|
||||
} else {
|
||||
pos = i - 1
|
||||
}
|
||||
p.tiles[i] = NewTile(i, pos)
|
||||
}
|
||||
|
||||
for x := range fieldSymX {
|
||||
for y := range fieldSymY {
|
||||
p.blink[x][y] = rand.IntN(len(frames))
|
||||
}
|
||||
}
|
||||
|
||||
p.requestStats()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (g *game) Tick(t ticker) {
|
||||
if t == ticker10Hz && g.solved {
|
||||
b := g.blinkCoef[0]
|
||||
for i := 1; i < len(g.blinkCoef); i++ {
|
||||
g.blinkCoef[i-1] = g.blinkCoef[i]
|
||||
}
|
||||
g.blinkCoef[len(g.blinkCoef)-1] = b
|
||||
if g.showCongrats() {
|
||||
for x := range fieldSymX {
|
||||
for y := range fieldSymY {
|
||||
g.blink[x][y] = (g.blink[x][y] + 1) % len(frames)
|
||||
g.color[x][y] = color.RGBA{uint8(rand.IntN(256)), uint8(rand.IntN(256)), uint8(rand.IntN(256)), 0xFF}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *game) SetLang(lc langCode) {
|
||||
g.langCode = lc
|
||||
}
|
||||
|
||||
func (g *game) ApiStatsHandler(s model.Stats) {
|
||||
g.stats.Store(s)
|
||||
}
|
||||
|
||||
func (g *game) Draw(s Screen) {
|
||||
drawGameField(s)
|
||||
if g.isSolved() {
|
||||
rating := l10nRank(g.langCode) + ": "
|
||||
wins := l10nWins(g.langCode) + ": "
|
||||
if !g.withStats(func(s model.Stats) {
|
||||
if s.Rank > 0 && s.GamesSolved > 0 {
|
||||
rating += fmt.Sprint(s.Rank)
|
||||
} else {
|
||||
rating = ""
|
||||
}
|
||||
wins += fmt.Sprint(s.GamesSolved)
|
||||
}) {
|
||||
rating, wins = "_", "_"
|
||||
}
|
||||
printHeader(s, rating, -1)
|
||||
printHeader(s, wins, 1)
|
||||
} else {
|
||||
printHeader(s, fmt.Sprintf(l10nSilent(g.langCode)+" %s", checkbox[g.muted]), 1)
|
||||
if !g.muted {
|
||||
printHeader(s, fmt.Sprintf(l10nMoves(g.langCode)+": %d", g.moves), -1)
|
||||
}
|
||||
}
|
||||
|
||||
if g.showCongrats() {
|
||||
for x := range fieldSymX {
|
||||
for y := range fieldSymY {
|
||||
s.Print(string(frames[g.blink[x][y]]), image.Point{x + 1, y + 3}, g.color[x][y])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
boardBottomColor := color.Black
|
||||
// boardBottomColor := color.RGBA{0xFF, 0x00, 0xFF, 0xFF}
|
||||
|
||||
for i := range tiles {
|
||||
var tileBackground color.Color = color.RGBA{0, 0, 0xA0, 0xFF} // background: 0x0000A0 (lighter) / 0x00006B (darker)
|
||||
var tileForeground color.Color = color.RGBA{0, 0xFF, 0xFF, 0xFF} // text: cyan 0x00FFFF
|
||||
fillRect := image.Rect(g.tiles[i].X(), g.tiles[i].Y(), g.tiles[i].X()+puzzleTileSymW, g.tiles[i].Y()+puzzleTileSymH)
|
||||
if i == 0 {
|
||||
if g.solved {
|
||||
cr, cg, cb, ca := tileForeground.RGBA()
|
||||
tileForeground = color.RGBA{byte(g.blinkCoef[0] * float64(cr)),
|
||||
byte(g.blinkCoef[0] * float64(cg)),
|
||||
byte(g.blinkCoef[0] * float64(cb)),
|
||||
byte(g.blinkCoef[0] * float64(ca))}
|
||||
} else {
|
||||
tileBackground = boardBottomColor
|
||||
fillRect.Max.X--
|
||||
}
|
||||
}
|
||||
if g.tiles[i].moving {
|
||||
// fillRect contains unshifted yet coordinates, time to fix font gallucinations when animating adjacent tiles
|
||||
fillRect.Max.X-- // right border: skip last column to justify on move
|
||||
s.Fill(fillRect, boardBottomColor) // partial "board bottom" from both sides
|
||||
fillRect.Max.X++ // right border: restore
|
||||
fillRect.Min.X-- // left border: extend to 1 col left to justify on move
|
||||
}
|
||||
s.Fill(fillRect, tileBackground)
|
||||
if i == 0 && g.solved || i > 0 {
|
||||
s.Print(tileTemplate.Format(g.tiles[i].Title()), image.Point{g.tiles[i].X(), g.tiles[i].Y()}, tileForeground)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *game) Interact(a Audio, col, row int, t time.Duration) (result actionResult) {
|
||||
result = resultNone
|
||||
if !g.isSolved() && row == 1 && col < puzzleSymX-2 && col > puzzleSymX-13 {
|
||||
g.muted = !g.muted
|
||||
return
|
||||
}
|
||||
if g.showCongrats() {
|
||||
g.moves = 0
|
||||
return
|
||||
}
|
||||
for i := range tiles {
|
||||
if g.tiles[i].CanInteract(col, row) {
|
||||
if g.tiles[i].num == 0 && t > time.Second*3 {
|
||||
return resultSwitchForm
|
||||
}
|
||||
if g.press(a, g.tiles[i]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (g *game) shuffle() {
|
||||
rand.Shuffle(tiles, func(i, j int) { g.tiles[i].pos, g.tiles[j].pos = g.tiles[j].pos, g.tiles[i].pos })
|
||||
var puzzle [tiles]byte
|
||||
for i := range g.tiles {
|
||||
puzzle[i] = byte(g.tiles[i].pos)
|
||||
}
|
||||
if solvable, err := IsSolvable(puzzle); err == nil && !solvable {
|
||||
g.tiles[1].pos, g.tiles[2].pos = g.tiles[2].pos, g.tiles[1].pos
|
||||
}
|
||||
g.moves = 0
|
||||
g.solved = g.isSolved()
|
||||
}
|
||||
|
||||
func (g *game) press(a Audio, t *tile) bool {
|
||||
if g.solved {
|
||||
if g.tiles[0].num == t.num {
|
||||
g.shuffle()
|
||||
}
|
||||
return true
|
||||
}
|
||||
exchange := func() { t.pos, g.tiles[0].pos = g.tiles[0].pos, t.pos; g.onMove(a) }
|
||||
switch {
|
||||
case t.Col() != g.tiles[0].Col() && t.Row() != g.tiles[0].Row():
|
||||
return false
|
||||
case t.Col() == g.tiles[0].Col() && g.tiles[0].Row()-t.Row() == 1:
|
||||
t.MoveDown(exchange)
|
||||
case t.Col() == g.tiles[0].Col() && t.Row()-g.tiles[0].Row() == 1:
|
||||
t.MoveUp(exchange)
|
||||
case t.Row() == g.tiles[0].Row() && g.tiles[0].Col()-t.Col() == 1:
|
||||
t.MoveRight(exchange)
|
||||
case t.Row() == g.tiles[0].Row() && t.Col()-g.tiles[0].Col() == 1:
|
||||
t.MoveLeft(exchange)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *game) onMove(a Audio) {
|
||||
if !g.muted {
|
||||
a.PlaySound()
|
||||
}
|
||||
if g.moves == 0 {
|
||||
g.onStart()
|
||||
}
|
||||
g.moves++
|
||||
solved := g.isSolved()
|
||||
if solved && !g.solved {
|
||||
g.onSolve(g.moves)
|
||||
}
|
||||
g.solved = solved
|
||||
}
|
||||
|
||||
func (g *game) isSolved() bool {
|
||||
for i := 1; i < tiles; i++ {
|
||||
if g.tiles[i].pos != i-1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return g.moves > 0
|
||||
}
|
||||
|
||||
func (g *game) isTopRated() bool {
|
||||
firstPlace := false
|
||||
firstPlace = g.withStats(func(s model.Stats) { firstPlace = s.Rank == 1 })
|
||||
return firstPlace
|
||||
}
|
||||
|
||||
func (g *game) showCongrats() bool {
|
||||
return g.isSolved() && g.isTopRated()
|
||||
}
|
||||
|
||||
func (g *game) withStats(consumer func(model.Stats)) bool {
|
||||
if stats := g.stats.Load(); stats != nil {
|
||||
consumer(stats.(model.Stats))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *game) Activate() {}
|
||||
|
||||
func IsSolvable(puzzle [tiles]byte) (bool, error) {
|
||||
var x *int
|
||||
for i := range puzzle {
|
||||
if puzzle[i] == 0 {
|
||||
if x != nil {
|
||||
return false, errors.New("only one blank position allowed")
|
||||
}
|
||||
x = &i
|
||||
}
|
||||
}
|
||||
if x == nil {
|
||||
return false, errors.New("blank position required to be set")
|
||||
}
|
||||
blank := *x
|
||||
invCount := 0
|
||||
for i := range tiles - 1 {
|
||||
for j := i + 1; j < tiles; j++ {
|
||||
if puzzle[j] > 0 && puzzle[i] > 0 && puzzle[i] > puzzle[j] {
|
||||
invCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if (blank/4+1)%2 == 0 {
|
||||
return invCount%2 == 0, nil
|
||||
} else {
|
||||
return invCount%2 == 1, nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user