skulls/skulls.go
2021-06-03 22:07:56 -04:00

703 lines
15 KiB
Go

package skulls
import (
"bytes"
"fmt"
"image"
"image/color"
"math/rand"
"time"
"github.com/hajimehoshi/ebiten/v2/inpututil"
// Required for Ebiten.
_ "image/jpeg"
_ "image/png"
"github.com/goki/freetype/truetype"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/audio"
"github.com/hajimehoshi/ebiten/v2/audio/mp3"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/text"
"github.com/rootVIII/skulls/assets"
"golang.org/x/image/font"
)
const (
screenW = 640
screenH = 960
frame = 64
rowMax = 22
colMax = 15
hotspotUpX = 123.0
hotspotUpY = 788.0
hotspotLeftX = 39.0
hotspotLeftY = 862.0
hotspotRightX = 210.0
hotspotRightY = 865.0
hotspotButtonX = 373.0
hotspotButtonY = 818.0
)
// Clock is the global game clock.
var Clock float64
// Game controls overall gameplay.
type Game struct {
background, explosion, intro *ebiten.Image
skulls map[string]*ebiten.Image
planchette, onDeck []string
skullColors []string
skullCollector [][]string
skullCoords [][][]int
empties [][2]int
searchHead [2]int
beep, clear, track *audio.Player
fontFace font.Face
green color.Color
isMovingL, isMovingR, isMovingU bool
havePlanchette, isPlaying bool
wonGame, lostGame bool
rollCount, moveCount, explosionCount int
jumpCount, jumpMax, loseCount int
score, level, best, matchMin int
}
/* - - - - - - - - U P D A T E M E T H O D S - - - - - - - - */
func (g *Game) rollBones() {
g.planchette = append(g.planchette[len(g.planchette)-1:], g.planchette[:len(g.planchette)-1]...)
row, col := g.searchHead[0], g.searchHead[1]
for _, skull := range g.planchette {
g.skullCollector[row][col] = skull
row++
}
}
func (g *Game) removeCurrentPos() {
row, col := g.searchHead[0], g.searchHead[1]
for range g.planchette {
g.skullCollector[row][col] = ""
row++
}
}
func (g *Game) shiftPlanchette() {
row, col := g.searchHead[0], g.searchHead[1]
for _, skull := range g.planchette {
g.skullCollector[row][col] = skull
row++
}
}
func (g *Game) matchSkulls() [][2]int {
return append(g.checkCols(), g.checkRows()...)
}
func (g *Game) removeEmpties() {
for _, coords := range g.empties {
g.skullCollector[coords[0]][coords[1]] = ""
}
}
func (g *Game) checkRows() [][2]int {
var matchesIndex = make([][2]int, 0)
var matchColor string
var matchCount int
for x := 0; x < len(g.skullCollector[0]); x++ {
matchColor = ""
matchCount = 0
for y := 0; y < len(g.skullCollector); y++ {
if g.skullCollector[y][x] == matchColor {
matchCount++
} else {
matchCount = 0
}
var last = (y == len(g.skullCollector)-1) || (g.skullCollector[y+1][x] != g.skullCollector[y][x])
if matchCount > 2 && last && len(matchColor) > 0 {
for index := y - matchCount; index <= y; index++ {
matchesIndex = append(matchesIndex, [2]int{index, x})
}
}
matchColor = g.skullCollector[y][x]
}
}
return matchesIndex
}
func (g *Game) checkCols() [][2]int {
var matchesIndex = make([][2]int, 0)
var matchColor string
var matchCount int
for ri, row := range g.skullCollector {
matchColor = ""
matchCount = 0
for ci, col := range row {
if col == matchColor {
matchCount++
} else {
matchCount = 0
}
if matchCount > 2 && (ci == len(row)-1 || g.skullCollector[ri][ci+1] != col) && len(matchColor) > 1 {
for index := ci - matchCount; index <= ci; index++ {
matchesIndex = append(matchesIndex, [2]int{ri, index})
}
}
matchColor = col
}
}
return matchesIndex
}
func (g *Game) bubbleSortSkulls() {
var length = len(g.skullCollector)
for x := 0; x < len(g.skullCollector[0]); x++ {
for {
var madeChange = false
for y := 0; y < length; y++ {
for i := 0; i < length-y-1; i++ {
if len(g.skullCollector[y][x]) < 1 && len(g.skullCollector[y+1][x]) > 1 {
g.skullCollector[y][x], g.skullCollector[y+1][x] = g.skullCollector[y+1][x], g.skullCollector[y][x]
madeChange = true
}
}
}
if !madeChange {
break
}
}
}
}
func (g *Game) isValidMove(direction byte) bool {
row, col := g.searchHead[0], g.searchHead[1]
switch direction {
case 0x4C:
if col < 1 || len(g.skullCollector[row][col-1]) > 0 {
return false
}
case 0x52:
if col > 13 || len(g.skullCollector[row][col+1]) > 0 {
return false
}
case 0x55:
if row < 1 || len(g.skullCollector[row-1][col]) > 0 {
for {
var combined = g.matchSkulls()
if len(combined) > 0 {
g.clear.Play()
g.score += len(combined)
g.best = g.score
g.empties = combined
g.explosionCount = 30
g.removeEmpties()
g.bubbleSortSkulls()
g.clear.Rewind()
} else {
break
}
}
g.reset()
return false
}
}
return true
}
func (g Game) inHotSpot(courseX, courseY float64) bool {
touchX, touchY := ebiten.TouchPosition(0)
if float64(touchX) < courseX || float64(touchX) > courseX+frame {
return false
}
if float64(touchY) < courseY || float64(touchY) > courseY+frame {
return false
}
return true
}
func (g Game) hotspotClickedLeft() bool {
return g.inHotSpot(hotspotLeftX, hotspotLeftY)
}
func (g Game) hotspotClickedRight() bool {
return g.inHotSpot(hotspotRightX, hotspotRightY)
}
func (g Game) hotspotClickedUp() bool {
return g.inHotSpot(hotspotUpX, hotspotUpY)
}
func (g Game) hotspotClickedButton() bool {
return g.inHotSpot(hotspotButtonX, hotspotButtonY)
}
func (g *Game) updatePlanchette() {
if !g.lostGame && g.hotspotClickedLeft() {
g.isMovingL = true
g.isMovingR = false
g.isMovingU = false
}
if !g.lostGame && g.hotspotClickedRight() {
g.isMovingR = true
g.isMovingL = false
g.isMovingU = false
}
if g.hotspotClickedUp() {
g.isMovingR = false
g.isMovingL = false
g.isMovingU = true
}
if g.isMovingL && g.moveCount < 1 && g.isValidMove('L') {
g.removeCurrentPos()
g.searchHead[1] -= 1
g.shiftPlanchette()
}
if g.isMovingR && g.moveCount < 1 && g.isValidMove('R') {
g.removeCurrentPos()
g.searchHead[1] += 1
g.shiftPlanchette()
}
if (g.isMovingU && g.moveCount < 1 || g.jumpCount == 0) && g.isValidMove('U') {
g.removeCurrentPos()
g.searchHead[0] -= 1
g.shiftPlanchette()
}
if inpututil.IsTouchJustReleased(0) && (g.isMovingL || g.isMovingR || g.isMovingU) {
g.isMovingL, g.isMovingR, g.isMovingU = false, false, false
}
if g.hotspotClickedButton() {
if g.rollCount < 1 {
g.beep.Play()
g.rollBones()
g.beep.Rewind()
}
g.rollCount++
}
if g.isMovingL || g.isMovingR || g.isMovingU {
g.moveCount++
}
if g.rollCount > 9 {
g.rollCount = 0
}
if g.moveCount > 7 {
g.moveCount = 0
}
if g.jumpCount > g.jumpMax {
g.jumpCount = 0
} else {
g.jumpCount++
}
}
func (g *Game) spawn() {
g.onDeck = nil
maxLen := randNo(1, 5)
for i := 0; i < maxLen; i++ {
g.onDeck = append(g.onDeck, g.skullColors[randNo(0, 4)])
}
}
func (g *Game) deepCopyPlanchette() {
g.planchette = nil
g.planchette = append(g.planchette, g.onDeck...)
}
func (g *Game) insertPlanchette() {
row, col := 21, 8
for i := len(g.planchette) - 1; i >= 0; i-- {
if len(g.skullCollector[row][col]) > 0 {
g.lostGame = true
g.isPlaying = false
}
g.skullCollector[row][col] = g.planchette[i]
row--
}
g.searchHead[0], g.searchHead[1] = row+1, col
}
func (g *Game) initSkullCollector() {
g.skullCollector = make([][]string, rowMax)
for row := 0; row < rowMax; row++ {
cols := make([]string, colMax)
for col := 0; col < colMax; col++ {
cols[col] = ""
}
g.skullCollector[row] = cols
}
}
func (g *Game) initSkullCoords() {
g.skullCoords = make([][][]int, rowMax)
for outer, y := 0, 60; y < (rowMax+1)*32; outer, y = outer+1, y+32 {
g.skullCoords[outer] = make([][]int, colMax)
for inner, x := 0, 22; x < colMax*32; inner, x = inner+1, x+32 {
g.skullCoords[outer][inner] = make([]int, 2)
g.skullCoords[outer][inner] = []int{x, y}
}
}
}
func (g *Game) initPlanchettes() {
g.planchette = make([]string, 0)
g.onDeck = make([]string, 0)
}
func (g *Game) reset() {
g.havePlanchette = false
g.isMovingL = false
g.isMovingR = false
g.isMovingU = false
g.rollCount = 1
g.moveCount = 0
g.jumpCount = 1
switch score := g.score; {
case score > 99999:
g.wonGame = true
case score > 250:
g.level = 7
g.jumpMax = 14
case score > 100:
g.level = 6
g.jumpMax = 16
case score > 50:
g.level = 5
g.jumpMax = 18
case score > 40:
g.level = 4
g.jumpMax = 20
case score > 30:
g.level = 3
g.jumpMax = 25
case score > 20:
g.level = 2
g.jumpMax = 30
case score > 10:
g.level = 1
g.jumpMax = 35
}
}
func (g *Game) resetGame() {
g.score = 0
g.reset()
g.loseCount = 300
g.lostGame = false
g.isPlaying = false
g.wonGame = false
}
func (g *Game) checkTrackPlaying() {
if !g.track.IsPlaying() {
g.track.Rewind()
g.track.Play()
}
}
/* - - - - - - - - D R A W M E T H O D S - - - - - - - - */
func (g Game) drawBackground(screen *ebiten.Image) {
opts := &ebiten.DrawImageOptions{}
opts.GeoM.Translate(0, 0)
screen.DrawImage(g.background, opts)
}
func (g Game) drawAllGameText(screen *ebiten.Image) {
text.Draw(screen, fmt.Sprintf("%05d", g.score), g.fontFace, 528, 610, g.green)
text.Draw(screen, fmt.Sprintf("%05d", g.best), g.fontFace, 528, 788, g.green)
text.Draw(screen, fmt.Sprintf("%02d", g.level), g.fontFace, 556, 926, g.green)
}
func (g Game) drawOnDeck(screen *ebiten.Image) {
var opts *ebiten.DrawImageOptions
var odY float64
switch len(g.onDeck) {
case 4:
odY = 186.00
case 3:
odY = 209.00
case 2:
odY = 222.00
case 1:
odY = 244.00
}
for _, skull := range g.onDeck {
opts = &ebiten.DrawImageOptions{}
opts.GeoM.Translate(561.00, odY)
screen.DrawImage(g.skulls[skull], opts)
odY += 32.00
}
}
func (g Game) drawSkullCollector(screen *ebiten.Image) {
var opts *ebiten.DrawImageOptions
for i, row := range g.skullCollector {
for j, col := range row {
if len(col) > 0 {
opts = &ebiten.DrawImageOptions{}
opts.GeoM.Translate(float64(g.skullCoords[i][j][0]), float64(g.skullCoords[i][j][1]))
screen.DrawImage(g.skulls[col], opts)
}
}
}
}
func (g *Game) drawExplosions(screen *ebiten.Image) {
var opts *ebiten.DrawImageOptions
var width = frame / 2
if g.explosionCount > 0 {
for _, coords := range g.empties {
opts = &ebiten.DrawImageOptions{}
opts.GeoM.Translate(float64(g.skullCoords[coords[0]][coords[1]][0]), float64(g.skullCoords[coords[0]][coords[1]][1]))
i := int(Clock/5) % 8
sX, sY := i*width, 0
screen.DrawImage(g.explosion.SubImage(image.Rect(sX, sY, sX+width, sY+width)).(*ebiten.Image), opts)
g.explosionCount--
}
}
}
func (g Game) drawIntro(screen *ebiten.Image) {
opts := &ebiten.DrawImageOptions{}
opts.GeoM.Translate(0, 0)
screen.DrawImage(g.intro, opts)
if int(Clock)%50 < 40 {
text.Draw(screen, "TOUCH TO BEGIN", g.fontFace, 200, 890, color.White)
}
}
func (g *Game) drawLostGame(screen *ebiten.Image) {
if g.loseCount > 0 {
text.Draw(screen, "Game Over", g.fontFace, 200, 400, color.White)
g.loseCount--
}
}
func (g Game) drawWonGame(screen *ebiten.Image) {
text.Draw(screen, " You Win ", g.fontFace, 200, 400, color.White)
}
/* - - - - - - - - E B I T E N M E T H O D S - - - - - - - - */
// Update proceeds the game state every tick (1/60 [s] by default).
func (g *Game) Update() error {
Clock++
if g.isPlaying {
if !g.havePlanchette {
g.deepCopyPlanchette()
g.insertPlanchette()
g.spawn()
g.havePlanchette = true
}
g.updatePlanchette()
} else if g.lostGame && g.loseCount < 1 {
g.resetGame()
g.initSkullCollector()
g.initPlanchettes()
g.spawn()
} else if g.wonGame {
g.isPlaying = false
} else {
if inpututil.IsTouchJustReleased(0) {
g.isPlaying = true
Clock = 0
}
}
return nil
}
// Layout takes the outside/window size and returns the (logical) screen size.
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return screenW, screenH
}
// Draw the screen every frame (typically 1/60[s] for 60Hz display).
func (g *Game) Draw(screen *ebiten.Image) {
g.checkTrackPlaying()
if g.lostGame {
g.drawBackground(screen)
g.drawSkullCollector(screen)
g.drawAllGameText(screen)
g.drawLostGame(screen)
} else if g.wonGame {
g.drawBackground(screen)
g.drawAllGameText(screen)
g.drawWonGame(screen)
} else if !g.isPlaying {
g.drawIntro(screen)
} else {
g.drawBackground(screen)
g.drawOnDeck(screen)
g.drawSkullCollector(screen)
g.drawExplosions(screen)
g.drawAllGameText(screen)
}
ebitenutil.DebugPrint(screen, "")
}
/* - - - - - - - - U T I L I T Y F U N C T I O N S - - - - - - - - */
func randNo(min, max int) int {
rand.Seed(time.Now().UnixNano())
return rand.Intn(max-min) + min
}
func readRawIMG(asset []byte) (*ebiten.Image, error) {
rawIMG, _, err := image.Decode(bytes.NewReader(asset))
if err != nil {
return nil, err
}
newImage := ebiten.NewImageFromImage(rawIMG)
return newImage, nil
}
func readAudio(context *audio.Context, asset []byte) (*audio.Player, error) {
mp3Decoded, err := mp3.Decode(context, bytes.NewReader(asset))
if err != nil {
return nil, err
}
player, err := audio.NewPlayer(context, mp3Decoded)
if err != nil {
return nil, err
}
return player, nil
}
// Load is the entry point to the game.
func Load() (*Game, error) {
audioContext := audio.NewContext(44100)
radioLand, err := truetype.Parse(assets.RadioLandTTF)
if err != nil {
return nil, err
}
PNGs := [][]byte{
assets.BackgroundPNG,
assets.ExplosionPNG,
assets.IntroPNG,
assets.GreenskullPNG,
assets.RedskullPNG,
assets.PurpleskullPNG,
assets.BlueskullPNG,
}
var images []*ebiten.Image
for _, png := range PNGs {
ebImage, err := readRawIMG(png)
if err != nil {
return nil, err
}
images = append(images, ebImage)
}
beepSound, err := readAudio(audioContext, assets.BeepMP3)
if err != nil {
return nil, err
}
clearSound, err := readAudio(audioContext, assets.ClearMP3)
if err != nil {
return nil, err
}
themeSong, err := readAudio(audioContext, assets.ThemeMP3)
if err != nil {
return nil, err
}
var game = &Game{
background: images[0],
explosion: images[1],
intro: images[2],
beep: beepSound,
clear: clearSound,
track: themeSong,
fontFace: truetype.NewFace(radioLand, &truetype.Options{Size: 28, DPI: 72, Hinting: font.HintingFull}),
green: color.RGBA{R: 0x7C, G: 0xFC, B: 0x00, A: 0xFF},
skullColors: []string{"purple", "blue", "red", "green"},
jumpCount: 1,
jumpMax: 35,
matchMin: 3,
loseCount: 300,
}
game.skulls = map[string]*ebiten.Image{
"green": images[3],
"red": images[4],
"purple": images[5],
"blue": images[6],
}
game.initSkullCollector()
game.initSkullCoords()
game.initPlanchettes()
game.track.SetVolume(.70)
game.clear.SetVolume(.50)
game.beep.SetVolume(.50)
game.spawn()
ebiten.SetWindowSize(screenW, screenH)
ebiten.SetWindowTitle("💀")
return game, nil
}