pixel-examples/community/amidakuji/game.go

775 lines
21 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"math"
"math/rand"
"os"
"reflect"
"strings"
"sync"
"time"
"unsafe"
gg "github.com/faiface/pixel-examples/community/amidakuji/glossary"
"github.com/faiface/pixel-examples/community/amidakuji/glossary/jukebox"
glfw "github.com/go-gl/glfw/v3.2/glfw"
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
"github.com/faiface/pixel/text"
"github.com/sqweek/dialog"
"golang.org/x/image/colornames"
)
// Actor updates and draws itself. It acts as a game object.
type Actor interface {
Drawer
Updater
}
// Drawer draws itself.
type Drawer interface {
Draw()
}
// Updater updates itself.
type Updater interface {
Update()
}
// -------------------------------------------------------------------------
// Core game
// game is a path finder.
// Also it manages and draws everything about...
type game struct {
// something system, somthing runtime
window *pixelgl.Window // lazy init
bg pixel.RGBA
camera *gg.Camera // lazy init
fpsw *gg.FPSWatch
dtw gg.DtWatch
vsync <-chan time.Time // lazy init
// game state
isRefreshedLadder bool
isRefreshedNametags bool
isScalpelMode bool
// drawings
mutex sync.Mutex // It is unsafe to access any refd; ptrd object without a critical section.
nPlayers int
ladder *Ladder
scalpel *Scalpel
paths []Path
emojis []pixel.Sprite
nametagPicks Nametags
nametagPrizes Nametags
atlas *text.Atlas
galaxy *gg.Galaxy
explosions *gg.Explosions
// other user settings
fontSize float64
winWidth float64 // The screen width, not the game width.
winHeight float64
initialZoomLevel float64
initialRotateDegree float64
}
type gameConfig struct {
nParticipants int
nLevel int
winWidth float64
winHeight float64
width float64
height float64
initialZoomLevel float64
initialRotateDegree float64
paddingTop float64
paddingRight float64
paddingBottom float64
paddingLeft float64
fontSize float64
nametagPicks []string
nametagPrizes []string
}
// init game
func newGame(cfg gameConfig) *game {
newEmojis := func(nParticipants int) (emojis []pixel.Sprite) {
emojis = make([]pixel.Sprite, nParticipants)
const dir = "emoji"
randomNames, err := gg.AssetDir(dir) // The order is random because they're from a map.
if err != nil {
return nil
}
nRandomNames := len(randomNames)
for participant := 0; participant < nParticipants; participant++ {
emojis[participant] = *gg.NewSprite(dir + "/" + randomNames[participant%nRandomNames]) // val, not ptr
}
return emojis
}
g := game{
bg: gg.RandomNiceColor(),
fpsw: gg.NewFPSWatchSimple(pixel.V(cfg.winWidth, cfg.winHeight), gg.Top, gg.Right),
isRefreshedLadder: false,
isRefreshedNametags: false,
isScalpelMode: false,
nPlayers: cfg.nParticipants,
ladder: NewLadder(
cfg.nParticipants, cfg.nLevel,
cfg.width, cfg.height,
cfg.paddingTop, cfg.paddingRight,
cfg.paddingBottom, cfg.paddingLeft,
),
scalpel: &Scalpel{},
paths: make([]Path, cfg.nParticipants),
emojis: newEmojis(cfg.nParticipants),
nametagPicks: make([]Nametag, cfg.nParticipants), // val, not ptr
nametagPrizes: make([]Nametag, cfg.nParticipants), // val, not ptr
atlas: gg.NewAtlas(
"", cfg.fontSize,
[]rune(strings.Join(cfg.nametagPicks, "")+strings.Join(cfg.nametagPrizes, "")),
), // A prepared set of images of characters or symbols to be drawn.
galaxy: gg.NewGalaxy(cfg.width, cfg.height, 400),
explosions: gg.NewExplosions(cfg.width, cfg.width, nil, 5),
initialZoomLevel: cfg.initialZoomLevel,
initialRotateDegree: cfg.initialRotateDegree,
winWidth: cfg.winWidth,
winHeight: cfg.winHeight,
}
// init paths
g.ResetPaths()
// copy nametags
copyNametagPicks := func(dstNametags []Nametag, srcNames []string) {
positions := g.ladder.PtsAtLevelOfPicks()
for i := 0; i < cfg.nParticipants; i++ {
posAdjust := positions[i]
posAdjust.Y += 5
posAdjust.X -= 60
dstNametags[i] = *NewNametagSimple(
g.atlas, "", posAdjust,
gg.Middle, gg.Right,
) // val, not ptr
if i < len(srcNames) {
dstNametags[i].desc = srcNames[i]
}
}
}
copyNametagPrizes := func(dstNametags []Nametag, srcNames []string) {
positions := g.ladder.PtsAtLevelOfPrizes()
for i := 0; i < cfg.nParticipants; i++ {
posAdjust := positions[i]
posAdjust.Y += 5
posAdjust.X += 15
dstNametags[i] = *NewNametagSimple(
g.atlas, "", posAdjust,
gg.Middle, gg.Left,
) // val, not ptr
if i < len(srcNames) {
dstNametags[i].desc = srcNames[i]
}
}
}
copyNametagPicks(g.nametagPicks, cfg.nametagPicks)
copyNametagPrizes(g.nametagPrizes, cfg.nametagPrizes)
// log.Println(g.nametagPicks[1].desc) //
return &g
}
func (g *game) Draw() {
g.mutex.Lock()
defer g.mutex.Unlock()
// This was originally an argument of this function.
var t pixel.BasicTarget
t = g.window
// ---------------------------------------------------
// 1. canvas a game world
t.SetMatrix(g.camera.Transform())
// Draw()s in an order.
g.galaxy.Draw(t)
g.ladder.Draw(t)
for iPath := range g.paths {
g.paths[iPath].Draw(t)
}
g.nametagPicks.Draw(t)
g.nametagPrizes.Draw(t)
if g.explosions.IsExploding() {
g.explosions.Draw(t)
}
for iEmoji := range g.emojis {
g.emojis[iEmoji].Draw(
t, pixel.IM.
Scaled(pixel.ZV, 2).
Rotated(pixel.ZV, -g.camera.Angle()).
Moved(g.paths[iEmoji].PosTip()),
)
}
if g.isScalpelMode {
g.scalpel.Draw(t)
}
if g.isScalpelMode {
UpdateDrawUnprojekt(g.window, g.ladder.bound, colornames.Blue, g.camera.Transform())
UpdateDrawUnprojekt2(g.window, g.ladder.bound, colornames.Red, *g.camera)
}
// ---------------------------------------------------
// 2. canvas a screen
t.SetMatrix(pixel.IM)
// Draw()s in an order.
g.fpsw.Draw(g.window)
if g.isScalpelMode {
UpdateDrawProjekt(g.window, g.ladder.bound, colornames.Black, g.camera.Transform())
}
}
func (g *game) Update(dt float64) {
g.mutex.Lock()
defer g.mutex.Unlock()
// The camera would and should update every frame.
g.camera.Update(dt)
// Update only if there is a need.
// isRefreshedLadder be set to false if there was an update to the ladder or its scalpel.
if !g.isRefreshedLadder {
g.ladder.Update()
g.scalpel.Update(*g.ladder)
g.isRefreshedLadder = true
}
// Only update when there is a need.
if !g.isRefreshedNametags {
g.nametagPicks.Update()
g.nametagPrizes.Update()
g.isRefreshedNametags = true
}
// Only currently animating paths need to update each frame.
for iPath := range g.paths {
if g.paths[iPath].IsAnimating() {
g.paths[iPath].Update(g.ladder.colors[iPath])
}
}
// As long as it doesn't hurt the framerate.
if g.fpsw.GetFPS() >= 10 {
g.galaxy.Update(dt)
}
// Only update when there is at least one (animating) explosion.
if g.explosions.IsExploding() {
g.explosions.Update(dt)
}
}
func (g *game) OnResize(width, height float64) {
g.camera.SetScreenBound(pixel.R(0, 0, width, height))
g.fpsw.SetPos(pixel.V(width, height), gg.Top, gg.Right)
// g.explosions.SetBound(width, height)
}
// -------------------------------------------------------------------------
// Single path
// ClearPath of a participant.
func (g *game) ClearPath(participant int) {
g.paths[participant] = *NewPathEmpty()
}
// ResetPath of a participant.
func (g *game) ResetPath(participant int) {
// GeneratePath contains a path-finding algorithm. This function is used as a path finder.
GeneratePath := func(g *game, participant int) Path {
const icol int = 0 // level
irow := participant // participant
grid := g.ladder.grid
route := []pixel.Vec{}
prize := -1
for level := icol; level < g.ladder.nLevel; level++ {
route = append(route, grid[irow][level])
prize = irow
if irow+1 < g.ladder.nParticipants {
if g.ladder.bridges[irow][level] {
irow++ // cross the bridge ... to the left (south)
route = append(route, grid[irow][level])
prize = irow
continue
}
}
if irow-1 >= 0 {
if g.ladder.bridges[irow-1][level] {
irow-- // cross the bridge ... to the right (north)
route = append(route, grid[irow][level])
prize = irow
continue
}
}
}
// log.Println(participant, prize, irow) //
// A path found here is called a route or roads.
return *NewPath(route, &prize) // val, not ptr
}
g.paths[participant] = GeneratePath(g, participant) // path-find
g.paths[participant].OnPassedEachPoint = func(pt pixel.Vec, dir pixel.Vec) {
g.explosions.ExplodeAt(pt, dir.Scaled(2))
}
}
func (g *game) AnimatePath(participant int) {
g.paths[participant].Animate()
}
func (g *game) AnimatePathInTime(participant int, sec float64) {
g.paths[participant].AnimateInTime(sec)
}
// -------------------------------------------------------------------------
// All paths
func (g *game) ResetPaths() {
g.mutex.Lock()
defer g.mutex.Unlock()
for participant := 0; participant < g.nPlayers; participant++ {
g.ResetPath(participant)
}
}
// AnimatePaths in order.
func (g *game) AnimatePaths(thunkAnimatePath func(participant int)) {
g.mutex.Lock()
defer g.mutex.Unlock()
for participant := 0; participant < g.nPlayers; participant++ {
participantCurr := participant
participantNext := participant + 1
prize := g.paths[participantCurr].GetPrize()
title := "Result"
caption := fmt.Sprint(
" 👆 Pick\t(No. ", participantCurr+1, ")\t", g.nametagPicks[participantCurr], "\t",
"\r\n", "\r\n",
" 🎁 Prize\t(No. ", prize+1, ")\t", g.nametagPrizes[prize], "\t",
"\r\n",
)
g.paths[participant].OnFinishedAnimation = func() {
if g.window.Monitor() == nil {
dialog.Message("%s", caption).Title(title).Info()
}
if participantNext < g.nPlayers {
thunkAnimatePath(participantNext)
}
g.paths[participantCurr].OnFinishedAnimation = nil
}
}
thunkAnimatePath(0)
}
// -------------------------------------------------------------------------
// Game controls
func (g *game) Reset() {
g.ladder.Reset()
g.ResetPaths()
g.isRefreshedLadder = false
}
// Shuffle in an approximate time.
func (g *game) Shuffle(times, inMillisecond int) {
speed := g.galaxy.Speed()
g.galaxy.SetSpeed(speed * 10)
{
i := 0
for range time.Tick(
(time.Millisecond * time.Duration(inMillisecond)) / time.Duration(times),
) {
g.bg = gg.RandomNiceColor()
g.Reset()
i++
if i >= times {
break
}
}
}
g.galaxy.SetSpeed(speed)
}
// Pause the game.
func (g *game) Pause() {
for i := range g.paths {
if g.paths[i].IsAnimating() {
g.paths[i].Pause()
}
}
}
// Resume after pause.
func (g *game) Resume() {
g.dtw.Dt()
for i := range g.paths {
if g.paths[i].IsAnimating() {
g.paths[i].Resume()
}
}
}
func (g *game) SetFullScreenMode(on bool) {
if on {
monitor := pixelgl.PrimaryMonitor()
width, height := monitor.Size()
// log.Println(monitor.VideoModes()) //
g.window.SetMonitor(monitor)
go func(width, height float64) {
g.OnResize(width, height)
}(width, height)
} else if !on { // off
g.window.SetMonitor(nil)
} else {
panic(errors.New("it may be thread"))
}
}
// -------------------------------------------------------------------------
// Read only methods
// WindowDeep is a hacky way to access a window in deep.
// It returns (window *glfw.Window) which is an unexported member inside a (*pixelgl.Window).
// Read only argument game ignores the pass lock by value warning.
func (g game) WindowDeep() (baseWindow *glfw.Window) {
return *(**glfw.Window)(unsafe.Pointer(reflect.Indirect(reflect.ValueOf(g.window)).FieldByName("window").UnsafeAddr()))
}
// Read only argument game ignores the pass lock by value warning.
func (g game) BridgesCount() (sum int) {
for _, row := range g.ladder.bridges {
for _, col := range row {
if col {
sum++
}
}
}
return sum
}
// -------------------------------------------------------------------------
// Run on main thread
// Run the game window and its event loop on main thread.
func (g *game) Run() {
pixelgl.Run(func() {
g.RunLazyInit()
g.RunEventLoop()
})
}
func (g *game) RunLazyInit() {
// This window will show up as soon as it is created.
win, err := pixelgl.NewWindow(pixelgl.WindowConfig{
Title: title + " (" + version + ")",
Icon: nil,
Bounds: pixel.R(0, 0, g.winWidth, g.winHeight),
Monitor: nil,
Resizable: true,
// Undecorated: true,
VSync: false,
})
if err != nil {
panic(err)
}
win.SetSmooth(true)
MoveWindowToCenterOfPrimaryMonitor := func(win *pixelgl.Window) {
vmodes := pixelgl.PrimaryMonitor().VideoModes()
vmodesLast := vmodes[len(vmodes)-1]
biggestResolution := pixel.R(0, 0, float64(vmodesLast.Width), float64(vmodesLast.Height))
win.SetPos(biggestResolution.Center().Sub(win.Bounds().Center()))
}
MoveWindowToCenterOfPrimaryMonitor(win)
// lazy init vars
g.window = win
g.camera = gg.NewCamera(g.ladder.bound.Center(), g.window.Bounds())
// register callback
windowGL := g.WindowDeep()
windowGL.SetSizeCallback(func(_ *glfw.Window, width int, height int) {
g.OnResize(float64(width), float64(height))
})
// time manager
g.vsync = time.Tick(time.Second / 120)
g.fpsw.Start()
g.dtw.Start()
// so-called loading
{
g.window.Clear(colornames.Brown)
screenCenter := g.window.Bounds().Center()
txt := text.New(screenCenter, gg.NewAtlas("", 36, nil))
txt.WriteString("Loading...")
txt.Draw(g.window, pixel.IM)
g.window.Update()
}
g.NextFrame(g.dtw.Dt()) // Give it a blood pressure.
g.NextFrame(g.dtw.Dt()) // Now the oxygenated blood will start to pump through its vein.
// Do whatever you want after that...
// from user setting
g.camera.Zoom(float64(g.initialZoomLevel))
g.camera.Rotate(g.initialRotateDegree)
}
func (g *game) RunEventLoop() {
for g.window.Closed() != true { // Your average event loop in main thread.
// Notice that all function calls as go routine are non-blocking, but the others will block the main thread.
// ---------------------------------------------------
// 0. dt
dt := g.dtw.Dt()
// ---------------------------------------------------
// 1. handling events
g.HandlingEvents(dt)
// ---------------------------------------------------
// 2. move on
g.NextFrame(dt)
// log.Println(g.window.Closed()) //
} // for
} // func
func (g *game) HandlingEvents(dt float64) {
// Notice that all function calls as go routine are non-blocking, but the others will block the main thread.
// system
if g.window.JustReleased(pixelgl.KeyEscape) {
g.window.SetClosed(true)
}
if g.window.JustReleased(pixelgl.KeySpace) {
g.Pause()
dialog.Message("%s", "Pause").Title("PPAP").Info()
g.Resume()
}
if g.window.JustReleased(pixelgl.KeyTab) {
if g.window.Monitor() == nil {
g.SetFullScreenMode(true)
} else {
g.SetFullScreenMode(false)
}
}
// scalpel mode
if g.window.JustReleased(pixelgl.MouseButtonRight) {
go func() {
g.isScalpelMode = !g.isScalpelMode
}()
}
if g.window.JustReleased(pixelgl.MouseButtonLeft) {
// ---------------------------------------------------
if !jukebox.IsPlaying() {
jukebox.Play()
}
// ---------------------------------------------------
posWin := g.window.MousePosition()
posGame := g.camera.Unproject(posWin)
go func() {
g.explosions.ExplodeAt(pixel.V(posGame.X, posGame.Y), pixel.V(10, 10))
}()
// ---------------------------------------------------
if g.isScalpelMode {
// strTitle := fmt.Sprint(posGame.X, ", ", posGame.Y) //
strDlg := fmt.Sprint(
"number of bridges: ", g.BridgesCount(), "\r\n", "\r\n",
"camera angle in degree: ", (g.camera.Angle()/math.Pi)*180, "\r\n", "\r\n",
"camera coordinates: ", g.camera.XY().X, g.camera.XY().Y, "\r\n", "\r\n",
"game clock: ", g.dtw.GetTimeStarted(), "\r\n", "\r\n",
"starfield speed: ", g.galaxy.Speed(), "\r\n", "\r\n",
"mouse click coords in screen pos: ", posWin.X, posWin.Y, "\r\n", "\r\n",
"mouse click coords in game pos: ", posGame.X, posGame.Y,
)
go func() {
// g.window.SetTitle(strTitle) //
dialog.Message("%s", strDlg).Title("MouseButtonLeft").Info()
}()
}
}
// game ctrl
if g.window.JustReleased(pixelgl.Key1) { // shuffle
go func() {
g.Shuffle(10, 750)
}()
}
if g.window.JustReleased(pixelgl.Key2) { // find path slow
go func() {
g.ResetPaths()
g.AnimatePaths(g.AnimatePath)
}()
}
if g.window.JustReleased(pixelgl.Key3) { // find path fast
go func() {
g.ResetPaths()
g.AnimatePaths(func(participant int) {
g.AnimatePathInTime(participant, 1)
})
}()
}
// camera
if g.window.JustReleased(pixelgl.KeyEnter) {
go func() {
g.camera.Rotate(-90)
}()
}
if g.window.Pressed(pixelgl.KeyRight) {
go func(dt float64) { // This camera will go diagonal while the case is in middle of rotating the camera.
g.camera.Move(pixel.V(1000*dt, 0).Rotated(-g.camera.Angle()))
}(dt)
}
if g.window.Pressed(pixelgl.KeyLeft) {
go func(dt float64) {
g.camera.Move(pixel.V(-1000*dt, 0).Rotated(-g.camera.Angle()))
}(dt)
}
if g.window.Pressed(pixelgl.KeyUp) {
go func(dt float64) {
g.camera.Move(pixel.V(0, 1000*dt).Rotated(-g.camera.Angle()))
}(dt)
}
if g.window.Pressed(pixelgl.KeyDown) {
go func(dt float64) {
g.camera.Move(pixel.V(0, -1000*dt).Rotated(-g.camera.Angle()))
}(dt)
}
{ // if scrolled
zoomLevel := g.window.MouseScroll().Y
go func() {
g.camera.Zoom(zoomLevel)
}()
}
}
func (g *game) NextFrame(dt float64) {
// ---------------------------------------------------
// 1. update - calc state of game objects each frame
g.Update(dt)
g.fpsw.Poll()
// ---------------------------------------------------
// 2. draw on window
g.window.Clear(g.bg) // clear canvas
g.Draw() // then draw
// ---------------------------------------------------
// 3. update window - always end with it
g.window.Update()
<-g.vsync
}
// -------------------------------------------------------------------------
// On compile
const title = "AMIDA KUJI"
var version = "undefined"
// -------------------------------------------------------------------------
// Entry point
func main() {
defer func() {
err := jukebox.Finalize()
if err != nil {
log.Fatal(err)
}
}()
rand.Seed(time.Now().UnixNano())
conf := askConf()
if conf == nil {
conf = map[string]interface{}{
"window_width": 800.0,
"window_height": 800.0,
"max_player": 10.0,
"max_level": 100.0,
"width": 1500.0,
"height": 1500.0,
"zoom": -4.0,
"rotate_degree": 270.0,
"margin_top": 50.0,
"margin_right": 100.0,
"margin_bottom": 50.0,
"margin_left": 200.0,
"font_size": 28.0,
"picks": []interface{}{"Bulbasaur", "Ivysaur", "Venusaur", "Charmander", "Charmeleon", "Charizard", "Squirtle", "Wartortle", "Blastoise", "Caterpie", "Metapod", "Butterfree", "Weedle", "Kakuna", "Beedrill", "Pidgey", "Pidgeotto", "Pidgeot", "Rattata"},
"prizes": []interface{}{"TM88", "TM89", "TM90", "TM91", "TM92", "HM01", "HM02", "HM03", "HM04", "HM05", "HM06"},
}
}
newGame(gameConfig{
winWidth: conf["window_width"].(float64),
winHeight: conf["window_height"].(float64),
nParticipants: int(conf["max_player"].(float64)),
nLevel: int(conf["max_level"].(float64)),
width: conf["width"].(float64),
height: conf["height"].(float64),
initialZoomLevel: conf["zoom"].(float64),
initialRotateDegree: conf["rotate_degree"].(float64),
paddingTop: conf["margin_top"].(float64),
paddingRight: conf["margin_right"].(float64),
paddingBottom: conf["margin_bottom"].(float64),
paddingLeft: conf["margin_left"].(float64),
fontSize: conf["font_size"].(float64),
nametagPicks: gg.ItfsToStrs(conf["picks"].([]interface{})),
nametagPrizes: gg.ItfsToStrs(conf["prizes"].([]interface{})),
}).Run()
}
func askConf() (conf map[string]interface{}) {
for { // Load JSON
cwd, _ := os.Getwd()
filepath, err := dialog.File().Title("Load User Settings").
Filter("JSON Format (*.json)", "json").
Filter("All Files (*.*)", "*").
SetStartDir(cwd).Load()
if err != nil {
if err.Error() == "Cancelled" {
conf = nil
break
}
dialog.Message("%s", "Invalid file path."+"\r\n"+"\r\n"+fmt.Sprint(err)).Title("Failed to load JSON").Error()
continue
}
bytes, err := ioutil.ReadFile(filepath)
if err != nil {
dialog.Message("%s", "Could not read the file."+"\r\n"+"\r\n"+fmt.Sprint(err)).Title("Failed to load JSON").Error()
continue
}
err = json.Unmarshal(bytes, &conf)
if err != nil {
dialog.Message("%s", "The file is not valid JSON format."+"\r\n"+"\r\n"+fmt.Sprint(err)).Title("Failed to load JSON").Error()
continue
}
break
}
return
}