add community example: amidakuji

This commit is contained in:
Nanite Factory 2018-08-26 21:45:01 +09:00
parent 1f3ab6edb9
commit d50fdb3565
62 changed files with 2978 additions and 0 deletions

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,45 @@
OUT := amidakuji
ASSET_TARGET := glossary/asset.go
ASSET_SOURCE_DIR := assets
VERSION := $(shell git describe --always --long)
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
GO_FILES := $(shell find . -name '*.go' | grep -v /vendor/)
all: build build_windows
${ASSET_TARGET}:
go-bindata -o "${ASSET_TARGET}" -pkg "glossary" -prefix "${ASSET_SOURCE_DIR}" ${ASSET_SOURCE_DIR}/emoji ${ASSET_SOURCE_DIR}/karaoke ${ASSET_SOURCE_DIR}/NanumBarunGothic.ttf
build: ${ASSET_TARGET}
go build -i -v -o ${OUT} -ldflags "-w -s -X main.version=${VERSION}"
build_windows: ${ASSET_TARGET}
go build -i -v -o ${OUT}.exe -ldflags "-w -s -X main.version=${VERSION} -H windowsgui"
run: build
./${OUT}
test:
@go test -short ${PKG_LIST}
vet:
@go vet -copylocks=false ${PKG_LIST}
vet_annoying:
@go vet ${PKG_LIST}
lint:
@for file in ${GO_FILES} ; do \
golint $$file ; \
done
#static: vet lint
# go build -i -v -o ${OUT}-${VERSION} -ldflags "-extldflags \"-static\" -w -s -X main.version=${VERSION}"
#static_windows: vet lint
# go build -i -v -o ${OUT}-${VERSION}.exe -ldflags "-extldflags \"-static\" -w -s -X main.version=${VERSION} -H windowsgui"
clean:
-@rm ${ASSET_TARGET} ${OUT} ${OUT}.exe #${OUT}-*
.PHONY: build build_windows run vet vet_annoying lint clean

View File

@ -0,0 +1,93 @@
# [AMIDA KUJI](https://github.com/NaniteFactory/amidakuji/tree/1bf57c3639e4e5628d96d9171ed9e679b658fadb)
> Ghost Leg (Chinese: 畫鬼腳), known in Japan as Amidakuji (阿弥陀籤, "Amida lottery", so named because the paper was folded into a fan shape resembling Amida's halo) or in Korea as Sadaritagi (사다리타기, literally "ladder climbing"), is a method of lottery designed to create random pairings between two sets of any number of things, as long as the number of elements in each set is the same. This is often used to distribute things among people, where the number of things distributed is the same as the number of people. For instance, chores or prizes could be assigned fairly and randomly this way.
(Explanation from [Wikipedia](https://en.wikipedia.org/wiki/Ghost_Leg))
- - -
## Examples
| [GIF1](examples/user_conf_sample6.json) | [GIF2](examples/user_conf_sample3.json) |
| --- | --- |
| ![1](examples/1.gif) | ![2](examples/2.gif) |
| GIF3 |
| --- |
| ![3](examples/3.gif) |
- - -
## Control
| | Action | Input |
| --- | --- | --- |
|🔀 | Shuffle | 1 |
| ▶️ | Find path | 2 |
| ⏩ | Find path (faster) | 3 |
| ⏸ | Pause | Space |
| ⬆️➡️⬇️⬅️ | Move camera around | Arrow keys |
| ↩️ | Rotate camera | Enter |
| 🔭 | Zoom in and out | Scroll |
| 🎇 | Firework | Left click |
| 🔬 | Inspection | Right click |
| 🔁 | Toggle full screen mode | Tab |
- - -
## Build
#### Windows
```
$ go get -v github.com/faiface/pixel/examples/community/amidakuji/...
$ go get -v -u github.com/go-bindata/go-bindata/...
```
```
$ cd $GOPATH/src/github.com/faiface/pixel/examples/community/amidakuji/
$ make
```
#### Ubuntu
```
$ sudo apt-get clean
$ sudo rm -r /var/lib/apt/lists/*
$ sudo apt update
```
```
$ sudo apt install libglib2.0-dev libpango1.0-dev libasound2-dev libgdk-pixbuf2.0-dev libgl1-mesa-dev xorg-dev libgtk2.0-dev
```
```
$ go get -v github.com/faiface/pixel/examples/community/amidakuji/...
$ go get -v -u github.com/go-bindata/go-bindata/...
```
```
$ cd $GOPATH/src/github.com/faiface/pixel/examples/community/amidakuji/
$ make
```
- - -
## External sources
#### Library
- [Pixel](https://github.com/faiface/pixel/tree/7cff3ce3aed80129b7b1dd57e63439426e11b6ee)
- [Beep](https://github.com/faiface/beep/tree/63cc6fbbac46dba1a03e55f0ebc965d6c82ca8e1)
- [GLFW 3.2](https://github.com/go-gl/glfw/tree/513e4f2bf85c31fba0fc4907abd7895242ccbe50/v3.2/glfw)
- [dialog](https://github.com/sqweek/dialog/tree/2f9d9e5dd848a3bad4bdd0210c73bb90c13a3791)
#### Music
- [Night Tempo - Pure Present](https://nighttempo.bandcamp.com/album/pure-present) - [08 Kikuchi Momoko - Night Cruising (Night Tempo 100% Pure Remastered)](https://nighttempo.bandcamp.com/track/kikuchi-momoko-night-cruising-night-tempo-100-pure-remastered-2)
- [Night Tempo - Pure Present](https://nighttempo.bandcamp.com/album/pure-present) - [19 Takeuchi Mariya - Plastic Love (Night Tempo 100% Pure Remastered)](https://nighttempo.bandcamp.com/track/takeuchi-mariya-plastic-love-night-tempo-100-pure-remastered-3)
#### Image
- [Gophers...](https://github.com/egonelbre/gophers/tree/dfb1bc3e6092179bd80d2e4156a8d32dba484cc9)
#### Font
- [나눔바른고딕 (NanumBarunGothic.ttf)](https://hangeul.naver.com/2017/nanum)

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

View File

@ -0,0 +1,21 @@
{
"window_width": 600,
"window_height": 900,
"max_player": 4,
"max_level": 80,
"width": 1500,
"height": 1000,
"zoom": -3,
"rotate_degree": -90,
"margin_top": 50,
"margin_right": 100,
"margin_bottom": 50,
"margin_left": 150,
"font_size": 20,
"picks": ["Bulbasaur","Ivysaur","Venusaur","Charmander","Charmeleon","Charizard","Squirtle","Wartortle","Blastoise","Caterpie","Metapod","Butterfree","Weedle","Kakuna","Beedrill","Pidgey","Pidgeotto","Pidgeot","Rattata","Rattata"],
"prizes": ["None","Master Ball","Ultra Ball","Great Ball","Poke Ball","Safari Ball","Net Ball","Dive Ball","Nest Ball","Repeat Ball","Timer Ball","Luxury Ball","Premier Ball","Dusk Ball","Heal Ball","Quick Ball","Cherish Ball"]
}

View File

@ -0,0 +1,21 @@
{
"window_width": 1200,
"window_height": 800,
"max_player": 5,
"max_level": 100,
"width": 1500,
"height": 1000,
"zoom": -2,
"rotate_degree": -360,
"margin_top": 50,
"margin_right": 100,
"margin_bottom": 50,
"margin_left": 150,
"font_size": 20,
"picks": ["Psyduck","Golduck","Mankey","Primeape","Growlithe"],
"prizes": ["None","Potion","Antidote","Burn Heal","Ice Heal","Awakening","Paralyze Heal","Full Restore","Max Potion","Hyper Potion","Super Potion","Full Heal","Revive","Max Revive","Fresh Water","Soda Pop","Lemonade","Moomoo Milk"]
}

View File

@ -0,0 +1,21 @@
{
"window_width": 1800,
"window_height": 600,
"max_player": 50,
"max_level": 2000,
"width": 3000,
"height": 1000,
"zoom": -3,
"rotate_degree": -360,
"margin_top": 50,
"margin_right": 50,
"margin_bottom": 50,
"margin_left": 100,
"font_size": 20,
"picks": [],
"prizes": []
}

View File

@ -0,0 +1,21 @@
{
"window_width": 1800,
"window_height": 600,
"max_player": 8,
"max_level": 300,
"width": 3000,
"height": 1000,
"zoom": -3,
"rotate_degree": -720,
"margin_top": 50,
"margin_right": 50,
"margin_bottom": 50,
"margin_left": 100,
"font_size": 20,
"picks": ["피카츄", "꼬부기", "깨비참", "성원숭", "파이리", "날쌩마", "뚜벅쵸", "케이시", "망나뇽", "잠만보", "파라스", "덩쿠리", "뿔카노", "버터플", "꼬마돌", "ㅁ", "ㄴ", "ㅇ", "ㄹ", "a", "s", "d", "f"],
"prizes": ["꽝ㅠ", "당첨", "꽝ㅠ", "꽝ㅠ", "당첨", "꽝ㅠ", "꽝ㅠ", "당첨", "꽝ㅠ", "꽝ㅠ", "당첨", "꽝ㅠ", "꽝ㅠ", "당첨", "꽝ㅠ"]
}

View File

@ -0,0 +1,21 @@
{
"window_width": 1800,
"window_height": 600,
"max_player": 8,
"max_level": 300,
"width": 3000,
"height": 1000,
"zoom": -3,
"rotate_degree": -360,
"margin_top": 50,
"margin_right": 120,
"margin_bottom": 50,
"margin_left": 150,
"font_size": 18,
"picks": ["Tauros", "Magikarp", "Gyarados", "Lapras", "Ditto", "Eevee", "Vaporeon", "Jolteon", "Flareon", "Porygon", "Omanyte"],
"prizes": ["None","Master Ball","Ultra Ball","Great Ball","Poke Ball","Safari Ball","Net Ball","Dive Ball","Nest Ball","Repeat Ball","Timer Ball","Luxury Ball","Premier Ball","Dusk Ball","Heal Ball","Quick Ball","Cherish Ball"]
}

View File

@ -0,0 +1,21 @@
{
"window_width": 800.0,
"window_height": 800.0,
"max_player": 3.0,
"max_level": 20.0,
"width": 500.0,
"height": 500.0,
"zoom": 1.0,
"rotate_degree": 270.0,
"margin_top": 50.0,
"margin_right": 100.0,
"margin_bottom": 50.0,
"margin_left": 120.0,
"font_size": 28.0,
"picks": ["Tauros", "Magikarp", "Eevee", "Lapras", "Ditto", "Gyarados", "Vaporeon", "Jolteon", "Flareon", "Porygon", "Omanyte"],
"prizes": ["None","Master Ball","Ultra Ball","Great Ball","Poke Ball","Safari Ball","Net Ball","Dive Ball","Nest Ball","Repeat Ball","Timer Ball","Luxury Ball","Premier Ball","Dusk Ball","Heal Ball","Quick Ball","Cherish Ball"]
}

774
community/amidakuji/game.go Normal file
View File

@ -0,0 +1,774 @@
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
}

View File

@ -0,0 +1,150 @@
package glossary
import (
"math"
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
"golang.org/x/image/colornames"
)
// Camera is a tool to get the screen center to be able to follow a certain point on a plane.
type Camera struct {
anglePhysic float64 // Angle in radians (math.Pi)
angleFollow float64 // Angle expected to be in the near future.
zoomPosPhysic float64 // Z
zoomPosFollow float64 // Z expected to be in the near future.
planePosPhysic pixel.Vec // X, Y
planePosFollow pixel.Vec // X, Y expected to be in the near future.
screenBound pixel.Rect
moveSmooth bool
}
// NewCamera is a constructor.
func NewCamera(_pos pixel.Vec, _screenBound pixel.Rect) *Camera {
return &Camera{
anglePhysic: 0,
angleFollow: 0,
zoomPosPhysic: 1.0,
zoomPosFollow: 1.0,
planePosPhysic: _pos,
planePosFollow: _pos,
screenBound: _screenBound,
moveSmooth: true,
}
}
// -------------------------------------------------------------------------
// Read only
// Transform returns a transformation matrix of a camera.
// Use Transform().Project() to convert a game position to a screen position.
// To do the inverse operation, it is recommended to use Camera#Unproject() rather than Transform().Unproject()
func (camera Camera) Transform() pixel.Matrix {
return pixel.IM. // This transformation order is significant.
// ScaledXY(camera.planePos, pixel.V(camera.zoomPos, camera.zoomPos)).
Scaled(camera.planePosPhysic, camera.zoomPosPhysic). // Scaling
Rotated(camera.planePosPhysic, camera.anglePhysic). // Rotatation
Moved(camera.screenBound.Center().Sub(camera.planePosPhysic)) // Translation
}
// Unproject converts a screen position to a game position.
// This method is a replacement of Transform().Unproject() which might return a bit off position.
func (camera Camera) Unproject(screenPosition pixel.Vec) (gamePosition pixel.Vec) {
matrix1 := pixel.IM.
Scaled(camera.planePosPhysic, camera.zoomPosPhysic). // Scaling
Moved(camera.screenBound.Center().Sub(camera.planePosPhysic)) // Translation
matrix2 := pixel.IM.
Rotated(camera.planePosPhysic, -camera.anglePhysic) // Rotatation
return matrix2.Project(matrix1.Unproject(screenPosition))
}
// Angle returns the angle of a camera in radians.
func (camera Camera) Angle() float64 {
return camera.anglePhysic
}
// XYZ returns a camera's coordinates value X, Y, and Z in a current physical state.
func (camera Camera) XYZ() (float64, float64, float64) {
return camera.planePosPhysic.X, camera.planePosPhysic.Y, camera.zoomPosPhysic
}
// XY returns the X and Y of a camera as a vector.
func (camera Camera) XY() pixel.Vec {
return camera.planePosPhysic
}
// Z returns the zoom depth of a camera.
func (camera Camera) Z() float64 {
return camera.zoomPosPhysic
}
// -------------------------------------------------------------------------
// Read and Write
// Update a camera's current physical state (physics)
// by calculating coordinates X, Y, Z and its angle after delta time in seconds.
func (camera *Camera) Update(dt float64) {
if camera.moveSmooth { // lerp the camera position towards the target
angle := pixel.V(camera.anglePhysic, 0)
angleFollow := pixel.V(camera.angleFollow, 0)
angle = pixel.Lerp(angle, angleFollow, 1-math.Pow(1.0/128, dt))
camera.anglePhysic = angle.X
camera.planePosPhysic = pixel.Lerp(camera.planePosPhysic, camera.planePosFollow, 1-math.Pow(1.0/128, dt))
zoomPos := pixel.V(camera.zoomPosPhysic, 0)
zoomFollow := pixel.V(camera.zoomPosFollow, 0)
zoomPos = pixel.Lerp(zoomPos, zoomFollow, 1-math.Pow(1.0/128, dt))
camera.zoomPosPhysic = zoomPos.X
} else {
camera.anglePhysic = camera.angleFollow
camera.planePosPhysic = camera.planePosFollow
camera.zoomPosPhysic = camera.zoomPosFollow
}
}
// Rotate a camera by certain degrees.
// + ) Counterclockwise
// - ) Clockwise
func (camera *Camera) Rotate(degree float64) {
camera.angleFollow += degree * math.Pi / 180
}
// Zoom in and out with a camera by certain levels.
// + ) Zoom in
// - ) Zoom out
func (camera *Camera) Zoom(byLevel float64) {
const zoomAmount = 1.2
camera.zoomPosFollow *= math.Pow(zoomAmount, byLevel)
}
// Move camera a specified distance.
func (camera *Camera) Move(distance pixel.Vec) {
camera.planePosFollow = camera.planePosFollow.Add(distance)
}
// MoveTo () moves a camera to a point on a plane.
func (camera *Camera) MoveTo(posAim pixel.Vec) {
camera.planePosFollow = posAim
}
// SetScreenBound of a camera.
func (camera *Camera) SetScreenBound(screenBound pixel.Rect) {
camera.screenBound = screenBound
}
// -------------------------------------------------------------------
// Unnecessary
// Aim for experiments.
type Aim struct {
pos pixel.Vec
}
// Draw aim as a dot.
func (aim Aim) Draw(t pixel.Target) {
imd := imdraw.New(nil)
imd.Color = colornames.Red
imd.Push(aim.pos)
imd.Circle(10, 0)
imd.Draw(t)
}

View File

@ -0,0 +1,67 @@
package glossary
import (
"errors"
"time"
)
// DtWatch is a delta time checker.
type DtWatch struct {
init *time.Time
last *time.Time
}
// Start () is required in order to call other methods of a DtWatch.
func (watch *DtWatch) Start() {
byVal1 := time.Now()
byVal2 := byVal1
watch.init = &byVal1
watch.last = &byVal2
}
// IsStarted () determines whether it has started or not.
// .Start() is <not> required in order to call this method.
func (watch DtWatch) IsStarted() bool {
if watch.init == nil {
return false
} else if watch.init != nil {
return true
} else {
panic(errors.New("It might be thread"))
}
}
// GetTimeStarted gets the time it started.
// .Start() must be called prior to calling this method.
func (watch DtWatch) GetTimeStarted() time.Time {
return *watch.init
}
// SetTimeStarted sets the time it started.
// .Start() must be called prior to calling this method.
func (watch *DtWatch) SetTimeStarted(t time.Time) {
*watch.init = t
}
// Dt since last Dt() or DtNano().
// .Start() must be called prior to calling this method.
func (watch *DtWatch) Dt() (deltaTimeInSeconds float64) {
deltaTimeInSeconds = time.Since(time.Time(*watch.last)).Seconds()
*watch.last = time.Now()
return
}
// DtNano since last Dt() or DtNano().
// It returns a time instance with nanosecond precision.
// .Start() must be called prior to calling this method.
func (watch *DtWatch) DtNano() (deltaTimeInNanosec time.Duration) {
deltaTimeInNanosec = time.Since(time.Time(*watch.last))
*watch.last = time.Now()
return
}
// DtSinceStart is dt since last Start().
// .Start() must be called prior to calling this method.
func (watch DtWatch) DtSinceStart() (deltaTimeInSeconds float64) {
return time.Since(time.Time(*watch.init)).Seconds()
}

View File

@ -0,0 +1,198 @@
package glossary
import (
"image/color"
"math/rand"
"sync"
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
)
// -------------------------------------------------------------------------
// explosive.go
// - Original idea: "github.com/faiface/pixel/examples/community/bouncing"
// --------------------------------------------------------------------
// Explosions is an imdraw and a manager of all particles.
type Explosions struct {
imd *imdraw.IMDraw
mutex sync.Mutex // It is unsafe to access any refd; ptrd object without a critical section.
//
*colorPicker
width float64
height float64
particles []*particle
precision int
}
// NewExplosions is a constructor.
// The 3rd argument colors can be nil. Then it will use its default value of a color set.
func NewExplosions(width, height float64, colors []color.Color, precision int) *Explosions {
return &Explosions{
nil, sync.Mutex{},
newColorPicker(colors),
width, height, nil,
precision,
}
}
// SetBound of particles. All particles bounce when they meet this bound.
func (e *Explosions) SetBound(width, height float64) {
e.width = width
e.height = height
}
// IsExploding determines whether this Explosions is about to be updated or not.
// Pass lock by value warning from (e Explosions) should be ignored,
// because an Explosions here is just passed as a read only argument.
func (e Explosions) IsExploding() bool {
e.mutex.Lock()
defer e.mutex.Unlock()
return e.particles != nil
}
// Draw guarantees the thread safety, though it's not a necessary condition.
// It is quite dangerous to access this struct's member (imdraw) directly from outside these methods.
func (e *Explosions) Draw(t pixel.Target) {
e.mutex.Lock()
defer e.mutex.Unlock()
if e.imd == nil || len(e.particles) <= 0 { // isInvisible set to true.
return // An empty image is drawn.
}
e.imd.Draw(t)
}
// Update animates an Explosions. An Explosions is drawn on an imdraw.
func (e *Explosions) Update(dt float64) {
e.mutex.Lock()
defer e.mutex.Unlock()
// physics
aliveParticles := []*particle{}
for _, particle := range e.particles {
particle.update(dt, e.width, e.height)
if particle.life > 0 {
aliveParticles = append(aliveParticles, particle)
}
}
e.particles = aliveParticles
// imdraw (a state machine)
if e.imd == nil { // lazy creation
e.imd = imdraw.New(nil)
e.imd.EndShape = imdraw.RoundEndShape
e.imd.Precision = e.precision
}
imd := e.imd
imd.Clear()
// draw
for _, particle := range e.particles {
imd.Color = particle.color
imd.Push(particle.pos)
imd.Circle(16*particle.life, 0)
}
}
// ExplodeAt generates an explosion at given point.
func (e *Explosions) ExplodeAt(pos, vel pixel.Vec) {
e.mutex.Lock()
defer e.mutex.Unlock()
e.next()
e.particles = append(e.particles,
newParticleAt(pos, vel.Rotated(1).Scaled(rand.Float64()), e.here()),
newParticleAt(pos, vel.Rotated(2).Scaled(rand.Float64()), e.here()),
newParticleAt(pos, vel.Rotated(3).Scaled(rand.Float64()), e.here()),
newParticleAt(pos, vel.Rotated(4).Scaled(rand.Float64()), e.here()),
newParticleAt(pos, vel.Rotated(5).Scaled(rand.Float64()), e.here()),
newParticleAt(pos, vel.Rotated(6).Scaled(rand.Float64()), e.here()),
newParticleAt(pos, vel.Rotated(7).Scaled(rand.Float64()), e.here()),
newParticleAt(pos, vel.Rotated(8).Scaled(rand.Float64()), e.here()),
newParticleAt(pos, vel.Rotated(9).Scaled(rand.Float64()), e.here()),
newParticleAt(pos, vel.Rotated(10).Scaled(rand.Float64()+1), e.here()),
newParticleAt(pos, vel.Rotated(20).Scaled(rand.Float64()+1), e.here()),
newParticleAt(pos, vel.Rotated(30).Scaled(rand.Float64()+1), e.here()),
newParticleAt(pos, vel.Rotated(40).Scaled(rand.Float64()+1), e.here()),
newParticleAt(pos, vel.Rotated(50).Scaled(rand.Float64()+1), e.here()),
newParticleAt(pos, vel.Rotated(60).Scaled(rand.Float64()+1), e.here()),
newParticleAt(pos, vel.Rotated(70).Scaled(rand.Float64()+1), e.here()),
newParticleAt(pos, vel.Rotated(80).Scaled(rand.Float64()+1), e.here()),
newParticleAt(pos, vel.Rotated(90).Scaled(rand.Float64()+1), e.here()),
)
}
// --------------------------------------------------------------------
type particle struct {
pos pixel.Vec
vel pixel.Vec
color color.RGBA
life float64
}
func newParticleAt(pos, vel pixel.Vec, color color.RGBA) *particle {
color.A = 5
return &particle{pos, vel, color, rand.Float64() * 1.5}
}
func (p *particle) update(dt, width, height float64) {
p.pos = p.pos.Add(p.vel)
p.life -= 3 * dt
switch {
case p.pos.Y < 0 || p.pos.Y >= height:
p.vel.Y *= (-10 * dt)
case p.pos.X < 0 || p.pos.X >= width:
p.vel.X *= (-10 * dt)
}
}
// --------------------------------------------------------------------
type colorPicker struct {
colors []color.RGBA
index int
}
func newColorPicker(_colors []color.Color) *colorPicker {
if _colors == nil {
_colors = []color.Color{
color.RGBA{190, 38, 51, 255},
color.RGBA{224, 111, 139, 255},
color.RGBA{73, 60, 43, 255},
color.RGBA{164, 100, 34, 255},
color.RGBA{235, 137, 49, 255},
color.RGBA{247, 226, 107, 255},
color.RGBA{47, 72, 78, 255},
color.RGBA{68, 137, 26, 255},
color.RGBA{163, 206, 39, 255},
color.RGBA{0, 87, 132, 255},
color.RGBA{49, 162, 242, 255},
color.RGBA{178, 220, 239, 255},
}
}
colors := []color.RGBA{}
for _, v := range _colors {
if c, ok := v.(color.RGBA); ok {
colors = append(colors, c)
}
}
return &colorPicker{colors, 0}
}
func (colorPicker *colorPicker) next() color.RGBA {
if colorPicker.index++; colorPicker.index >= len(colorPicker.colors) {
colorPicker.index = 0
}
return colorPicker.colors[colorPicker.index]
}
func (colorPicker *colorPicker) here() color.RGBA {
return colorPicker.colors[colorPicker.index]
}

View File

@ -0,0 +1,134 @@
package glossary
import (
"fmt"
"image/color"
"sync"
"time"
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
"github.com/faiface/pixel/text"
"golang.org/x/image/colornames"
)
// FPSWatch measures the real-time frame rates and displays it on a target canvas.
type FPSWatch struct {
txt *text.Text // shared variable
atlas *text.Atlas // borrowed atlas for txt
imd *imdraw.IMDraw // shared variable
mutex sync.Mutex // synchronize
//
fps int // The FPS evaluated every second.
frames int // Frames count before the FPS update.
seccer <-chan time.Time // Ticks time every second.
//
desc string
pos pixel.Vec
anchorX AnchorX
anchorY AnchorY
colorBg color.Color
colorTxt color.Color
}
// NewFPSWatch is a constructor.
func NewFPSWatch(
additionalCaption string, _pos pixel.Vec,
_anchorY AnchorY, _anchorX AnchorX, // This is because the order is usually Y then X in spoken language.
_colorBg, _colorTxt color.Color,
) (watch *FPSWatch) {
return &FPSWatch{
atlas: AtlasASCII(),
fps: 0,
frames: 0,
seccer: nil,
desc: additionalCaption,
pos: _pos,
anchorX: _anchorX,
anchorY: _anchorY,
colorBg: _colorBg,
colorTxt: _colorTxt,
}
}
// NewFPSWatchSimple is a constructor.
func NewFPSWatchSimple(_pos pixel.Vec, _anchorY AnchorY, _anchorX AnchorX) *FPSWatch {
return NewFPSWatch("", _pos, _anchorY, _anchorX, colornames.Black, colornames.White)
}
// Start ticking every second.
func (watch *FPSWatch) Start() {
watch.seccer = time.Tick(time.Second)
}
// Poll () should be called only once and in every single frame. (Obligatory)
// This is an extended behavior of Update() like funcs.
func (watch *FPSWatch) Poll() {
watch.frames++
select {
case <-watch.seccer:
watch.fps = watch.frames
watch.frames = 0
go watch._Update()
default:
}
}
// SetPos to a position in screen coords.
func (watch *FPSWatch) SetPos(pos pixel.Vec, anchorY AnchorY, anchorX AnchorX) {
watch.pos = pos
watch.anchorX = anchorX
watch.anchorY = anchorY
}
// GetFPS returns the most recent FPS recorded.
// A non-ptr FPSWatch as a read only argument passes lock by value within itself but that seems totally fine.
func (watch FPSWatch) GetFPS() int {
return watch.fps
}
// Draw FPSWatch.
func (watch *FPSWatch) Draw(t pixel.Target) {
// lock before accessing txt & imdraw
watch.mutex.Lock()
defer watch.mutex.Unlock()
if watch.imd == nil && watch.txt == nil { // isInvisible set to true.
return // An empty image is drawn.
}
watch.imd.Draw(t)
watch.txt.Draw(t, pixel.IM)
}
// unexported
func (watch *FPSWatch) _Update() {
// lock before txt & imdraw update
watch.mutex.Lock()
defer watch.mutex.Unlock()
// text label (a state machine)
if watch.txt == nil { // lazy creation
watch.txt = text.New(pixel.ZV, watch.atlas)
}
txt := watch.txt
txt.Clear()
str := fmt.Sprint("FPS: ", watch.fps, " ", watch.desc)
AnchorTxt(txt, watch.pos, watch.anchorX, watch.anchorY, str)
txt.Color = watch.colorTxt
txt.Dot.X -= 1.0
txt.Dot.Y += 5.0
txt.WriteString(str)
// imdraw (a state machine)
if watch.imd == nil { // lazy creation
watch.imd = imdraw.New(nil)
}
imd := watch.imd
imd.Clear()
imd.Color = watch.colorBg
imd.Push(VerticesOfRect(txt.Bounds())...)
imd.Polygon(0)
}

View File

@ -0,0 +1,130 @@
package jukebox
import (
"errors"
"io/ioutil"
"os"
"sync"
"time"
gg "github.com/faiface/pixel/examples/community/amidakuji/glossary"
"github.com/faiface/beep"
"github.com/faiface/beep/speaker"
"github.com/faiface/beep/vorbis"
)
// -------------------------------------------------------------------------
const nMusics = 2
var (
mutex sync.Mutex
isPlaying bool
musics [nMusics]*_Music
)
// -------------------------------------------------------------------------
// singleton
func init() {
// city pop favorites
musics[0] = _NewMusicFromAsset("nighttempo-purepresent1", "karaoke/kikuchimomoko-nightcruising.ogg")
musics[1] = _NewMusicFromAsset("nighttempo-purepresent2", "karaoke/takeuchimariya-plasticlove.ogg")
// speaker on
speaker.Init(musics[0].format.SampleRate, musics[0].format.SampleRate.N(time.Second))
speaker.Play(beep.Iterate(func() (soundtrack beep.Streamer) {
musics[0].stream.Seek(0)
musics[1].stream.Seek(0)
return beep.Seq(musics[0].stream, musics[1].stream)
}))
speaker.Lock()
}
// IsPlaying determines whether the soundtrack is currently playing or not.
func IsPlaying() bool {
return isPlaying
}
// Play unlocks the speaker.
func Play() {
mutex.Lock()
defer mutex.Unlock()
if !isPlaying {
isPlaying = true
speaker.Unlock()
}
return
}
// Pause locks the speaker.
func Pause() {
mutex.Lock()
defer mutex.Unlock()
if isPlaying {
isPlaying = false
speaker.Lock()
}
return
}
// Finalize should be called on program exit.
// This function deletes the temporary music file its package generates.
func Finalize() error {
errs := ""
for _, music := range musics {
music.Close()
err := music._Destory()
if err != nil {
errs += " " + err.Error()
}
}
if errs != "" {
return errors.New(errs)
}
return nil
}
// -------------------------------------------------------------------------
// NewMusicFromAsset is a constructor.
func _NewMusicFromAsset(nameMusic, nameAsset string) *_Music {
asset, err := gg.Asset(nameAsset)
if err != nil {
// log.Fatal(err) //
}
return _NewMusic(nameMusic, asset)
}
// Music is a temporary file to play a single background music. It should be destroyed on program exit.
type _Music struct {
os.File
stream beep.StreamSeekCloser
format beep.Format
}
// NewMusic creates an instance of Music, a temporary file from which the speaker plays a music.
// speaker.Lock() to pause.
// speaker.Unlock() to resume/play.
func _NewMusic(name string, asset []byte) *_Music {
tmpfile, err := ioutil.TempFile("", name)
if err != nil {
// log.Fatal(err) //
}
// log.Println(tmpfile.Name()) //
_, err = tmpfile.Write(asset)
if err != nil {
// log.Fatal(err) //
}
stream, format, err := vorbis.Decode(tmpfile)
if err != nil {
// log.Fatal(err) //
}
return &_Music{*tmpfile, stream, format}
}
// Destory deletes the temporary music file.
func (music *_Music) _Destory() error {
return os.Remove(music.Name())
}

View File

@ -0,0 +1,166 @@
package glossary
import (
"image/color"
"math/rand"
"sync"
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
)
// -------------------------------------------------------------------------
// Reusable modified starfiled
// - Original: "github.com/faiface/pixel/examples/community/starfield"
// - Encapsulated by nanitefactory
// -------------------------------------------------------------------------
// Galaxy
type star struct {
Pos pixel.Vec // x, y
Z float64 // z
P float64 // prev z
C color.RGBA // color
}
// Galaxy is an imd of stars.
type Galaxy struct {
imd *imdraw.IMDraw // shared variable
mutex sync.Mutex // synchronize
//
width float64
height float64
speed float64
stars [1024]*star
}
// NewGalaxy is a constructor.
func NewGalaxy(_width, _height, _speed float64) *Galaxy {
return &Galaxy{
width: _width,
height: _height,
speed: _speed,
}
}
// Speed is a getter.
// Pass lock by value warning from (galaxy Galaxy) should be ignored,
// because a galaxy here is just passed as a read only argument.
func (galaxy Galaxy) Speed() float64 {
return galaxy.speed
}
// SetSpeed is a setter.
func (galaxy *Galaxy) SetSpeed(_speed float64) {
galaxy.speed = _speed
}
// Draw guarantees the thread safety, though it's not a necessary condition.
// It is quite dangerous to access this struct's member (imdraw) directly from outside these methods.
func (galaxy *Galaxy) Draw(t pixel.Target) {
galaxy.mutex.Lock()
defer galaxy.mutex.Unlock()
if galaxy.imd == nil { // isInvisible set to true.
return // An empty image is drawn.
}
galaxy.imd.Draw(t)
}
// Update animates a galaxy.
func (galaxy *Galaxy) Update(dt float64) {
// random()
random := func(min, max float64) float64 {
return rand.Float64()*(max-min) + min
}
// newStar()
newStar := func() *star {
starColors := []color.RGBA{
color.RGBA{157, 180, 255, 255},
color.RGBA{162, 185, 255, 255},
color.RGBA{167, 188, 255, 255},
color.RGBA{170, 191, 255, 255},
color.RGBA{175, 195, 255, 255},
color.RGBA{186, 204, 255, 255},
color.RGBA{192, 209, 255, 255},
color.RGBA{202, 216, 255, 255},
color.RGBA{228, 232, 255, 255},
color.RGBA{237, 238, 255, 255},
color.RGBA{251, 248, 255, 255},
color.RGBA{255, 249, 249, 255},
color.RGBA{255, 245, 236, 255},
color.RGBA{255, 244, 232, 255},
color.RGBA{255, 241, 223, 255},
color.RGBA{255, 235, 209, 255},
color.RGBA{255, 215, 174, 255},
color.RGBA{255, 198, 144, 255},
color.RGBA{255, 190, 127, 255},
color.RGBA{255, 187, 123, 255},
color.RGBA{255, 187, 123, 255},
} // Colors based on stellar types listed at // http://www.vendian.org/mncharity/dir3/starcolor/
return &star{
Pos: pixel.V(random(-galaxy.width, galaxy.width), random(-galaxy.height, galaxy.height)),
Z: random(0, galaxy.width),
P: 0,
C: starColors[rand.Intn(len(starColors))],
}
}
// lock before imdraw update
galaxy.mutex.Lock()
defer galaxy.mutex.Unlock()
// imdraw (a state machine)
if galaxy.imd == nil { // lazy creation
galaxy.imd = imdraw.New(nil)
galaxy.imd.SetMatrix(pixel.IM.Moved(pixel.V(galaxy.width/2, galaxy.height/2)))
}
imd := galaxy.imd
imd.Clear()
imd.Precision = 7
// now update all stars in this galaxy
for i, s := range galaxy.stars {
if s == nil {
galaxy.stars[i] = newStar()
s = galaxy.stars[i]
}
scale := func(unscaledNum, min, max, minAllowed, maxAllowed float64) float64 {
return (maxAllowed-minAllowed)*(unscaledNum-min)/(max-min) + minAllowed
}
s.P = s.Z
s.Z -= dt * galaxy.speed
if s.Z < 0 {
s.Pos.X = random(-galaxy.width, galaxy.width)
s.Pos.Y = random(-galaxy.height, galaxy.height)
s.Z = galaxy.width
s.P = s.Z
}
p := pixel.V(
scale(s.Pos.X/s.Z, 0, 1, 0, galaxy.width),
scale(s.Pos.Y/s.Z, 0, 1, 0, galaxy.height),
)
o := pixel.V(
scale(s.Pos.X/s.P, 0, 1, 0, galaxy.width),
scale(s.Pos.Y/s.P, 0, 1, 0, galaxy.height),
)
r := scale(s.Z, 0, galaxy.width, 11, 0)
galaxy.imd.Color = s.C
if p.Sub(o).Len() > 6 {
galaxy.imd.Push(p, o)
galaxy.imd.Line(r)
}
galaxy.imd.Push(p)
galaxy.imd.Circle(r, 0)
}
}

View File

@ -0,0 +1,215 @@
package glossary
import (
"bytes"
"fmt"
"image"
"io/ioutil"
"math"
"math/rand"
"os"
// Relevant packages of target format for a decoder must be initialized to register.
_ "image/gif"
_ "image/png"
"github.com/faiface/pixel"
"github.com/faiface/pixel/text"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"golang.org/x/image/font/basicfont"
)
var atlasASCII *text.Atlas
func init() {
atlasASCII = NewAtlas("", 18, nil)
}
// AtlasASCII returns an atlas which allows you to draw only ASCII characters.
// Atlas is a set of generated textures for glyphs in a specific font.
func AtlasASCII() *text.Atlas {
return atlasASCII
}
// NewAtlas newly loads and prepares a set of images of characters or symbols to be drawn.
// Arg runeSet would be set to nil if non-ASCII characters are not in use.
func NewAtlas(nameAssetTTF string, size float64, runeSet []rune) *text.Atlas {
if nameAssetTTF == "" {
nameAssetTTF = "NanumBarunGothic.ttf"
}
var face font.Face
asset, err := Asset(nameAssetTTF)
if err == nil {
face, err = LoadTrueTypeFont(asset, size)
}
if err != nil {
face = basicfont.Face7x13
}
return text.NewAtlas(face, text.ASCII, runeSet)
}
// NewSprite converts an asset (resource) into a sprite. Returns nil if there is an error.
// AssetNames() or AssetDir() might be helpful when utilizing this function.
func NewSprite(nameAsset string) *pixel.Sprite {
asset, err := Asset(nameAsset)
if err != nil {
// log.Println("1", err) //
return nil
}
pic, err := LoadPicture(asset)
if err != nil {
// log.Println("2", err) //
return nil
}
// log.Println("3", "success yay") //
return pixel.NewSprite(pic, pic.Bounds())
}
// LoadTrueTypeFontFromFile creates and returns a font face.
func LoadTrueTypeFontFromFile(path string, size float64) (font.Face, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
bytes, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
face, err := LoadTrueTypeFont(bytes, size)
if err != nil {
return nil, err
}
return face, nil
}
// LoadPictureFromFile decodes an image that has been encoded in a registered format. (png, jpg, etc.)
// Format registration is typically done by an init function in the codec-specific package. (with underscore import)
func LoadPictureFromFile(path string) (pixel.Picture, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
return nil, err
}
return pixel.PictureDataFromImage(img), nil
}
// LoadTrueTypeFont creates and returns a font face.
func LoadTrueTypeFont(bytes []byte, size float64) (font.Face, error) {
font, err := truetype.Parse(bytes)
if err != nil {
return nil, err
}
return truetype.NewFace(font, &truetype.Options{
Size: size,
GlyphCacheEntries: 1,
}), nil
}
// LoadPicture decodes an image that has been encoded in a registered format. (png, jpg, etc.)
// Format registration is typically done by an init function in the codec-specific package. (with underscore import)
func LoadPicture(_bytes []byte) (pixel.Picture, error) {
img, _, err := image.Decode(bytes.NewReader(_bytes))
if err != nil {
return nil, err
}
return pixel.PictureDataFromImage(img), nil
}
// RandomNiceColor from Platformer.
// Is not completely random without rand.Seed().
func RandomNiceColor() pixel.RGBA {
again:
r := rand.Float64()
g := rand.Float64()
b := rand.Float64()
len := math.Sqrt(r*r + g*g + b*b)
if len == 0 {
goto again
}
return pixel.RGB(r/len, g/len, b/len)
}
// VerticesOfRect returns 4 vertices of a rectangle in a form of a slice of vectors.
func VerticesOfRect(r pixel.Rect) []pixel.Vec {
return []pixel.Vec{
r.Min,
pixel.V(r.Max.X, r.Min.Y),
r.Max,
pixel.V(r.Min.X, r.Max.Y),
}
}
// ItfsToStrs converts []interface{} to []string.
func ItfsToStrs(itfs []interface{}) (strs []string) {
strs = make([]string, len(itfs))
for i, v := range itfs {
strs[i] = fmt.Sprint(v)
}
return strs
}
// Direction returns a direction as a normalized vector. This vector always has a length of 1.
func Direction(from, to pixel.Vec) (dirVecNormalized pixel.Vec) {
vec := to.Sub(from)
if vec.X == 0 && vec.Y == 0 {
return vec
}
return vec.Unit()
}
// -------------------------------------------------------------------------
// Anchors
// AnchorY - Top, Middle, Bottom
type AnchorY int
// enum AnchorY
const (
Top AnchorY = 1 + iota
Middle
Bottom
)
// AnchorX - Left, Center, Right
type AnchorX int
// enum AnchorX
const (
Left AnchorX = 1 + iota
Center
Right
)
// AnchorTxt positions a text.Text label with an anchor alignment.
func AnchorTxt(txt *text.Text, pos pixel.Vec, anchorX AnchorX, anchorY AnchorY, desc string) {
txt.Orig = pos
txt.Dot = pos
switch anchorX {
case Left:
txt.Dot.X -= 0
case Center:
txt.Dot.X -= (txt.BoundsOf(desc).W() / 2)
case Right:
txt.Dot.X -= txt.BoundsOf(desc).W()
}
switch anchorY {
case Top:
txt.Dot.Y -= txt.BoundsOf(desc).H()
case Middle:
txt.Dot.Y -= (txt.BoundsOf(desc).H() / 2)
case Bottom:
txt.Dot.Y -= 0
}
txt.Dot.X += 0
txt.Dot.Y += 0
}

View File

@ -0,0 +1,334 @@
package main
import (
"math/rand"
"sync"
gg "github.com/faiface/pixel/examples/community/amidakuji/glossary"
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
"golang.org/x/image/colornames"
)
// Ladder is an imdraw that does not animate at all,
// hence does not need to be modified every frame.
// Something kinda static and bone-like.
type Ladder struct {
imd *imdraw.IMDraw // shared variable
mutex sync.Mutex // synchronize
//
bound pixel.Rect
grid [][]pixel.Vec
bridges [][]bool
nParticipants int
nLevel int
paddingTop float64
paddingRight float64
paddingBottom float64
paddingLeft float64
colors []pixel.RGBA
}
// NewLadder is a constructor.
func NewLadder(_nParticipants, _nLevel int,
_width, _height,
_paddingTop, _paddingRight,
_paddingBottom, _paddingLeft float64) *Ladder {
// get random colors
colors := []pixel.RGBA{}
for i := 0; i < _nParticipants; i++ {
colors = append(colors, gg.RandomNiceColor())
}
// new grid
newGrid := func(nRow, nCol int) [][]pixel.Vec {
arr := make([][]pixel.Vec, nRow)
for i := range arr {
arr[i] = make([]pixel.Vec, nCol)
}
// log.Println(arr) //
return arr
}
// new bridges
newBridges := func(nParticipants, nLevel int) [][]bool {
nRow := nParticipants - 1
nCol := nLevel
arr := make([][]bool, nRow)
for i := range arr {
arr[i] = make([]bool, nCol)
}
return arr
}
// init ladder
l := Ladder{
imd: imdraw.New(nil),
bound: pixel.R(0, 0, _width, _height),
grid: newGrid(_nParticipants, _nLevel),
bridges: newBridges(_nParticipants, _nLevel),
nParticipants: _nParticipants,
nLevel: _nLevel,
paddingTop: _paddingTop,
paddingBottom: _paddingBottom,
paddingRight: _paddingRight,
paddingLeft: _paddingLeft,
colors: colors,
}
// init grid
updateGrid := func(l *Ladder) {
for participant := range l.grid { // row
for level := range l.grid[participant] { // col
y := l.Height() - (float64(participant) * l.DistParticipant()) // reverse
x := float64(level) * l.DistLevel()
y -= l.paddingTop
x += l.paddingLeft
l.grid[participant][level] = pixel.V(x, y)
}
}
} // Indices would not be aligned with the screen coordinates. (Reverse Y - Rows)
updateGrid(&l)
// log.Println(l.grid) //
return &l
}
// -------------------------------------------------------------------------
// Important methods
// Draw guarantees the thread safety, though it's not a necessary condition.
// It is quite dangerous to access this struct's member (imdraw) directly from outside these methods.
func (l *Ladder) Draw(t pixel.Target) {
l.mutex.Lock()
defer l.mutex.Unlock()
if l.imd == nil { // isInvisible set to true.
return // An empty image is drawn.
}
l.imd.Draw(t)
}
// Update draws a ladder on an imdraw.
func (l *Ladder) Update() {
ptsMovedAbout := func(sub pixel.Vec, pts ...pixel.Vec) (ptsMoved []pixel.Vec) {
ptsMoved = make([]pixel.Vec, len(pts))
copy(ptsMoved, pts)
for i, vec := range ptsMoved {
ptsMoved[i] = vec.Sub(sub)
}
// log.Println(ptsMoved) // debug
// log.Println(pts) // debug
return ptsMoved
}
ptsStart := l.PtsAtLevelOfPicks()
ptsEnd := l.PtsAtLevelOfPrizes()
circleRadius := 20.0
circlePts := ptsMovedAbout(pixel.V(circleRadius+10, 0), ptsStart...)
// lock shared imdraw access
l.mutex.Lock()
defer l.mutex.Unlock()
// imdraw (a state machine)
if l.imd == nil { // lazy creation
l.imd = imdraw.New(nil)
}
imd := l.imd
imd.Clear()
// draw lanes
imd.Color = colornames.White
imd.EndShape = imdraw.RoundEndShape
for i := range ptsStart {
imd.Push(ptsStart[i], ptsEnd[i])
imd.Line(13)
}
// draw bridges
imd.Color = colornames.White
imd.EndShape = imdraw.RoundEndShape
for nrow, row := range l.bridges {
for ncol, e := range row {
if e {
imd.Push(l.grid[nrow][ncol], l.grid[nrow+1][ncol])
imd.Line(13)
}
}
}
// draw start points
imd.EndShape = imdraw.RoundEndShape
for i := range ptsStart {
imd.Color = l.colors[i]
imd.Push(circlePts[i])
}
imd.Circle(circleRadius, 0)
}
// -------------------------------------------------------------------------
// Read only methods
// Height returns the height of a ladder.
// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine.
func (l Ladder) Height() float64 {
return l.bound.H()
}
// Width returns the width of a ladder.
// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine.
func (l Ladder) Width() float64 {
return l.bound.W()
}
// DistLevel returns the distance between each level.
// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine.
func (l Ladder) DistLevel() float64 {
return (l.Width() - (l.paddingLeft + l.paddingRight)) / float64(l.nLevel-1)
}
// DistParticipant returns the distance between each lane.
// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine.
func (l Ladder) DistParticipant() float64 {
return (l.Height() - (l.paddingTop + l.paddingBottom)) / float64(l.nParticipants-1)
}
// PtsAtLevelOfPicks returns all starting points of a ladder.
// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine.
func (l Ladder) PtsAtLevelOfPicks() (ret []pixel.Vec) {
const levelOfDraw int = 0 // where it starts
ret = make([]pixel.Vec, l.nParticipants, l.nParticipants)
for participant := range l.grid { // row
ret[participant] = l.grid[participant][levelOfDraw]
}
// log.Println(len(ret), ret) //
return ret //[:l.nParticipants] //
}
// PtAtLevelOfPicks returns a starting point of a ladder.
// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine.
func (l Ladder) PtAtLevelOfPicks(participant int) pixel.Vec {
const levelOfDraw int = 0 // where it starts
return l.grid[participant][levelOfDraw]
}
// PtsAtLevelOfPrizes returns all end points of a ladder.
// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine.
func (l Ladder) PtsAtLevelOfPrizes() (ret []pixel.Vec) {
levelOfPrize := l.nLevel - 1 // where it ends
ret = make([]pixel.Vec, l.nParticipants, l.nParticipants)
for participant := range l.grid { // row
ret[participant] = l.grid[participant][levelOfPrize]
}
// log.Println(len(ret), ret) //
return ret //[:l.nParticipants] //
}
// PtAtLevelOfPrizes returns an end point of a ladder.
// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine.
func (l Ladder) PtAtLevelOfPrizes(participant int) pixel.Vec {
levelOfPrize := l.nLevel - 1 // where it ends
return l.grid[participant][levelOfPrize]
}
// -------------------------------------------------------------------
// Methods that write to itself
// ClearBridges of a ladder.
// Only values are changed, not the pointers.
func (l *Ladder) ClearBridges() {
for _, row := range l.bridges {
for i := range row {
row[i] = false
}
}
}
// GenerateRandomBridges of an approximate amount.
// Only values are changed, not the pointers.
func (l *Ladder) GenerateRandomBridges(amountApprox int) {
pickOneBridgeInRandom := func(l *Ladder) {
nRow := len(l.bridges)
nCol := l.nLevel
row := rand.Intn(int(nRow)) // participant
col := rand.Intn(int(nCol)) // level
// check right
isOkRight := func(rowRight, col int) bool {
includeLowerBound := func(rowRight int) bool {
return rowRight >= 0
}
if !includeLowerBound(rowRight) { // out of bound
return true
}
if !l.bridges[rowRight][col] {
return true
}
return false
}
// check left
isOkLeft := func(rowLeft, col int) bool {
excludeUpperBound := func(rowLeft int) bool {
return rowLeft < len(l.bridges)
}
if !excludeUpperBound(rowLeft) { // out of bound
return true
}
if !l.bridges[rowLeft][col] {
return true
}
return false
}
rowRight := row - 1
rowLeft := row + 1
if isOkRight(rowRight, col) && isOkLeft(rowLeft, col) {
l.bridges[row][col] = true
}
} // func
// repeat
for i := 0; i < amountApprox; i++ {
pickOneBridgeInRandom(l)
}
} // method
// RegenerateRandomBridges clears out all bridges and then GenerateRandomBridges() an approximate amount.
// Only values are changed, not the pointers.
func (l *Ladder) RegenerateRandomBridges(amountApprox int) {
l.ClearBridges()
l.GenerateRandomBridges(amountApprox)
}
// RegenerateRandomColors sets all colors of a ladder random for each.
// Only values are changed, not the pointers.
func (l *Ladder) RegenerateRandomColors() {
for i := range l.colors {
l.colors[i] = gg.RandomNiceColor()
}
}
// Reset all bridges and colors.
// Only values are changed, not the pointers.
func (l *Ladder) Reset() {
aboutTwo := l.nParticipants * (l.nLevel - 1) * 2
aboutOne := l.nParticipants * (l.nLevel - 1)
aboutHalf := (l.nParticipants * (l.nLevel - 1)) / 2
//
var pick [4]int
pick[0] = aboutTwo
pick[1] = aboutOne
pick[2] = aboutHalf
i := rand.Intn(3)
// log.Println(i) //
//
l.RegenerateRandomBridges(pick[i])
l.RegenerateRandomColors()
}

View File

@ -0,0 +1,126 @@
package main
import (
"image/color"
"sync"
gg "github.com/faiface/pixel/examples/community/amidakuji/glossary"
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
"github.com/faiface/pixel/text"
"golang.org/x/image/colornames"
)
// Nametags is a list(slice) of nametags.
type Nametags []Nametag
// Update nametags.
func (updaters Nametags) Update() {
for i := range updaters {
updaters[i].Update()
}
}
// Draw nametags.
func (updaters Nametags) Draw(t pixel.Target) {
for i := range updaters {
updaters[i].Draw(t)
}
}
// Nametag for each nametag.
type Nametag struct {
txt *text.Text // shared variable
atlas *text.Atlas // borrowed atlas for txt
imd *imdraw.IMDraw // shared variable
mutex sync.Mutex // synchronize
//
desc string
pos pixel.Vec
anchorX gg.AnchorX
anchorY gg.AnchorY
colorBg color.Color
colorTxt color.Color
}
// NewNametag is a constructor.
func NewNametag(
_atlas *text.Atlas,
_desc string, _pos pixel.Vec,
_anchorY gg.AnchorY, // This is because the order is usually Y then X in spoken language.
_anchorX gg.AnchorX,
_colorBg, _colorTxt color.Color) *Nametag {
atlas := _atlas
if atlas == nil {
atlas = gg.AtlasASCII()
}
return &Nametag{
atlas: atlas,
desc: _desc,
pos: _pos,
anchorX: _anchorX,
anchorY: _anchorY,
colorBg: _colorBg,
colorTxt: _colorTxt,
}
}
// NewNametagSimple is a constructor.
func NewNametagSimple(
_atlas *text.Atlas,
_desc string, _pos pixel.Vec,
_anchorY gg.AnchorY,
_anchorX gg.AnchorX,
) *Nametag {
return NewNametag(_atlas, _desc, _pos, _anchorY, _anchorX, colornames.Wheat, colornames.Black)
}
// String of a nametag.
// A getter and a callback which allows a nametag to be passed to a function as a string.
// A non-ptr Nametag as a read only argument passes lock by value within itself but that seems totally fine.
func (n Nametag) String() string {
return n.desc
}
// Draw a nametag.
func (n *Nametag) Draw(t pixel.Target) {
n.mutex.Lock()
defer n.mutex.Unlock()
if n.imd == nil && n.txt == nil { // isInvisible set to true.
return // An empty image is drawn.
}
n.imd.Draw(t)
n.txt.Draw(t, pixel.IM)
}
// Update a nametag.
func (n *Nametag) Update() {
// lock before txt & imdraw update
n.mutex.Lock()
defer n.mutex.Unlock()
// text label (a state machine)
if n.txt == nil { // lazy creation
n.txt = text.New(pixel.ZV, n.atlas)
}
txt := n.txt
txt.Clear()
gg.AnchorTxt(txt, n.pos, n.anchorX, n.anchorY, n.desc)
txt.Color = n.colorTxt
txt.WriteString(n.desc)
// imdraw (a state machine)
if n.imd == nil { // lazy creation
n.imd = imdraw.New(nil)
}
imd := n.imd
imd.Clear()
imd.Color = n.colorBg
imd.Push(gg.VerticesOfRect(txt.Bounds())...)
imd.Polygon(0)
}

281
community/amidakuji/path.go Normal file
View File

@ -0,0 +1,281 @@
package main
import (
"math"
"sync"
gg "github.com/faiface/pixel/examples/community/amidakuji/glossary"
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
)
// Path is for animating a path to the prize in a ladder.
type Path struct {
imd *imdraw.IMDraw // shared variable
mutex sync.Mutex // synchronize
//
roads []pixel.Vec // A list of vectors - each vector for a position where a road starts.
prize *int
tip *pixel.Vec
tipDir pixel.Vec
iroad int
//
watchAnim gg.DtWatch // When it started to animate.
timeLimitAnimInSec float64
animateInTime bool
isAnimating bool
// -----------------------------------------------------------
// exported callbacks(listeners) regarding animation
// callback on reaching the prize level of a ladder.
OnFinishedAnimation func()
// callback when the animating 'tip' passes a point of a road.
// pt: a point(road) just passed.
// dir: ...
// dir is a normalized vector. (pixel.ZV) is passed if the direction can't be found.
// dir can be different depending on how fast this Path is updated.
OnPassedEachPoint func(pt pixel.Vec, dir pixel.Vec)
}
// NewPath is a contructor.
func NewPath(_roads []pixel.Vec, _prize *int) *Path {
newTip := func() *pixel.Vec {
if _roads != nil {
if len(_roads) > 0 {
v := _roads[0]
return &v
}
}
return nil
}
return &Path{
roads: _roads,
prize: _prize,
tip: newTip(),
tipDir: pixel.ZV,
}
}
// NewPathEmpty is a contructor.
func NewPathEmpty() *Path {
return &Path{}
}
// -------------------------------------------------------------------------
// Important methods
// Draw guarantees the thread safety, though it's not a necessary condition.
// It is quite dangerous to access this struct's member (imdraw) directly from outside these methods.
func (path *Path) Draw(t pixel.Target) {
path.mutex.Lock()
defer path.mutex.Unlock()
if path.imd == nil { // isInvisible set to true.
return // An empty image is drawn.
}
path.imd.Draw(t)
}
// Update animates a path. A path is drawn on an imdraw.
func (path *Path) Update(color pixel.RGBA) {
var (
iroad = len(path.roads) - 1
from = path.roads[len(path.roads)-1]
to = path.roads[len(path.roads)-1]
dir = pixel.ZV
)
if path.isAnimating {
// get where it is abstract // get a scalar
dt := path.watchAnim.DtSinceStart()
const distPerSec = 500
scalarProgress := dt * distPerSec
// log.Println(dt, path.Len(), scalarProgress) //
if path.animateInTime { // overwrite scalarProgress
fromLot := pixel.V(0, 0)
toPrize := pixel.V(path.Len(), 0)
percentagePointPerSec := 1 / path.timeLimitAnimInSec
scalarProgress = pixel.Lerp(fromLot, toPrize, dt*percentagePointPerSec).X
// log.Println(dt, path.Len(), scalarProgress, dt*percentagePointPerSec) //
}
// get where it is concrete // turn a scalar into a set of vectors
iroad, from, to, dir = path.FindRoadByDist(scalarProgress)
if iroad > path.iroad {
if path.OnPassedEachPoint != nil {
go path.OnPassedEachPoint(from, dir)
}
}
path.iroad = iroad
// log.Println(iroad, len(path.roads), iroad == len(path.roads)-1, path.isAnimating) //
if iroad >= len(path.roads)-1 { // the end
path.isAnimating = false
// log.Println(path.Len(), dt) //
if path.OnFinishedAnimation != nil {
go path.OnFinishedAnimation()
} // callback
}
}
// lock before imdraw update
path.mutex.Lock()
defer path.mutex.Unlock()
// imdraw (a state machine)
if path.imd == nil { // lazy creation
path.imd = imdraw.New(nil)
}
imd := path.imd
imd.Clear()
// draw path
imd.Color = color
imd.EndShape = imdraw.RoundEndShape
for i := 0; i < iroad; i++ {
imd.Push(path.roads[i], path.roads[i+1])
imd.Line(9)
}
imd.Push(from, to)
imd.Line(9)
// save where the tip is
path.tip = &to
}
// -------------------------------------------------------------------------
// Read only methods
// IsAnimating determines whether this Path is about to be updated or not.
// Pass lock by value warning from (path Path) should be ignored,
// because a Path here is just passed as a read only argument.
func (path Path) IsAnimating() bool {
return path.isAnimating
}
// GetPrize is just an average getter.
// It returns -1 if the receiver is not initialized with that member(prize).
// Pass lock by value warning from (path Path) should be ignored,
// because a Path here is just passed as a read only argument.
func (path Path) GetPrize() int {
if path.prize == nil {
return -1
}
return *path.prize
}
// PosTip returns a vector that tells you how far the animating path currently has reached.
// A non-ptr Path as a read only argument passes lock by value within itself but that seems totally fine.
func (path Path) PosTip() (v pixel.Vec) {
return *path.tip
}
// Len returns the total length of all roads.
// A non-ptr Path as a read only argument passes lock by value within itself but that seems totally fine.
func (path Path) Len() (sum float64) {
for i := 0; i < len(path.roads)-1; i++ {
sum += math.Abs(path.roads[i].Sub(path.roads[i+1]).Len())
}
return
}
// FindRoadByDist converts a scalar into a set of vectors.
// A non-ptr Path as a read only argument passes lock by value within itself but that seems totally fine.
//
// Returns
// iroad: The index of a road found.
// road: The vector representation of a road found. A road is a line from pt A to B, and that vector points to where pt A is.
// pos: A position(point) found which is in the middle of that found road(line).
// dirVecNormalized: A direction as a normalized vector. This vector always has a length of 1.
func (path Path) FindRoadByDist(distProgress float64) (iroad int, road pixel.Vec, pos pixel.Vec, dirVecNormalized pixel.Vec) {
lengthOfTraveledRoads := float64(0.0)
for iroad = 0; iroad < len(path.roads)-1; iroad++ {
var lengthOfThisRoad float64
iroadNext := iroad + 1
lengthOfThisRoad = math.Abs(path.roads[iroad].Sub(path.roads[iroadNext]).Len())
lengthOfTraveledRoads += lengthOfThisRoad
// For loop breaker: distProgress is somewhere between the total length of a path.
if lengthOfTraveledRoads > distProgress {
scalar := lengthOfThisRoad - (lengthOfTraveledRoads - distProgress)
if path.roads[iroad].Y == path.roads[iroadNext].Y &&
path.roads[iroad].X < path.roads[iroadNext].X { // to the bottom (east)
pos = path.roads[iroad]
pos.X += scalar
dirVecNormalized = pixel.V(1, 0)
} else if path.roads[iroad].X == path.roads[iroadNext].X &&
path.roads[iroad].Y > path.roads[iroadNext].Y { // to the left (south)
pos = path.roads[iroad]
pos.Y -= scalar
dirVecNormalized = pixel.V(0, -1)
} else if path.roads[iroad].X == path.roads[iroadNext].X &&
path.roads[iroad].Y < path.roads[iroadNext].Y { // to the right (north)
pos = path.roads[iroad]
pos.Y += scalar
dirVecNormalized = pixel.V(0, 1)
} else if path.roads[iroad].Y == path.roads[iroadNext].Y &&
path.roads[iroad].X > path.roads[iroadNext].X { // to the top (west)
// Placed at the end of an elif statement,
// since this case is of no possibility unless the path finding is going reverse.
pos = path.roads[iroad]
pos.X -= scalar
dirVecNormalized = pixel.V(-1, 0)
} else {
panic("unhandled exception: it may be a diagonal bridge")
} // elif
return iroad, path.roads[iroad], pos, dirVecNormalized
} // if - for loop breaker
} // for
// coming down to here means that the case is (road == pos)
from := iroad - 1
to := iroad
if iroad == 0 {
from = iroad
to = iroad + 1
}
if from < 0 || to >= len(path.roads) {
dirVecNormalized = pixel.ZV
} else {
dirVecNormalized = gg.Direction(path.roads[from], path.roads[to])
}
return iroad, path.roads[iroad], path.roads[iroad], dirVecNormalized
}
// -------------------------------------------------------------------------
// Methods that write to itself
// Animate a path.
func (path *Path) Animate() {
path.watchAnim.Start()
path.animateInTime = false
path.isAnimating = true
}
// AnimateInTime animates a path in given time.
func (path *Path) AnimateInTime(sec float64) {
path.watchAnim.Start()
path.timeLimitAnimInSec = sec
path.animateInTime = true
path.isAnimating = true
}
// Pause a path's clock.
func (path *Path) Pause() {
if path.watchAnim.IsStarted() {
path.watchAnim.Dt()
}
}
// Resume after pause.
func (path *Path) Resume() {
if path.watchAnim.IsStarted() {
started := path.watchAnim.GetTimeStarted()
dtPause := path.watchAnim.DtNano()
path.watchAnim.SetTimeStarted(started.Add(dtPause))
// log.Println(dtPause, started, path.watchAnim.GetTimeStarted()) //
}
}

View File

@ -0,0 +1,118 @@
package main
import (
"image/color"
"sync"
gg "github.com/faiface/pixel/examples/community/amidakuji/glossary"
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
"golang.org/x/image/colornames"
)
// Scalpel is a surgical knife for dissection and surgery. And for debugging purposes sometimes.
type Scalpel struct {
imd *imdraw.IMDraw // shared variable
mutex sync.Mutex // synchronize
}
// Draw guarantees the thread safety, though it's not a necessary condition.
// It is quite dangerous to access this struct's member (imdraw) directly from outside these methods.
func (s *Scalpel) Draw(t pixel.Target) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.imd == nil { // isInvisible set to true.
return // An empty image is drawn.
}
s.imd.Draw(t)
}
// Update dissects a ladder. The anatomy of a ladder is drawn on an imdraw.
// A non-ptr Ladder as a read only argument passes lock by value within itself but that seems totally fine.
func (s *Scalpel) Update(l Ladder) {
ptsEnd := l.PtsAtLevelOfPrizes()
// lock shared imdraw access
s.mutex.Lock()
defer s.mutex.Unlock()
// imdraw (a state machine)
if s.imd == nil { // lazy creation
s.imd = imdraw.New(nil)
}
imd := s.imd
imd.Clear()
// draw bounds
imd.Color = colornames.Black
imd.EndShape = imdraw.NoEndShape
imd.Push(gg.VerticesOfRect(l.bound)...)
imd.Polygon(4)
// draw end points
imd.Color = colornames.Blueviolet
imd.Push(ptsEnd...)
imd.Circle(10, 0)
// draw grid
imd.Color = colornames.Red
for nrow, row := range l.grid {
for ncol := range row {
imd.Push(l.grid[nrow][ncol])
}
}
imd.Circle(5, 0)
}
// -------------------------------------------------------------------------
// UpdateDrawProjekt has nothing to do with scalpel.
func UpdateDrawProjekt(t pixel.Target, rekt pixel.Rect, color color.Color, matrix pixel.Matrix) {
imd := imdraw.New(nil)
imd.Color = color
imd.EndShape = imdraw.NoEndShape
vertices := gg.VerticesOfRect(rekt)
for i, v := range vertices {
vertices[i] = matrix.Project(v)
}
imd.Push(vertices...)
imd.Polygon(10)
imd.Draw(t)
}
// UpdateDrawUnprojekt has nothing to do with scalpel.
func UpdateDrawUnprojekt(t pixel.Target, rekt pixel.Rect, color color.Color, matrix pixel.Matrix) {
imd := imdraw.New(nil)
imd.Color = color
imd.EndShape = imdraw.NoEndShape
vertices := gg.VerticesOfRect(rekt)
for i, v := range vertices {
vertices[i] = matrix.Unproject(v)
}
imd.Push(vertices...)
imd.Polygon(10)
imd.Draw(t)
}
// UpdateDrawUnprojekt2 has nothing to do with scalpel.
func UpdateDrawUnprojekt2(t pixel.Target, rekt pixel.Rect, color color.Color, camera gg.Camera) {
imd := imdraw.New(nil)
imd.Color = color
imd.EndShape = imdraw.NoEndShape
vertices := gg.VerticesOfRect(rekt)
for i, v := range vertices {
vertices[i] = camera.Unproject(v)
}
imd.Push(vertices...)
imd.Polygon(10)
imd.Draw(t)
}