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 }