├── static ├── ogp.png ├── main.js ├── screen-emulator.js └── index.html ├── images ├── flappygopher.jpg ├── jumpingopher.jpg └── flappygopher_miniten.png ├── examples ├── GeoM │ ├── geom │ │ ├── gopher.png │ │ └── game.go │ └── main.go └── drawing │ ├── main.go │ └── drawing │ └── drawing.go ├── games ├── goradius │ ├── goradius │ │ ├── beam.png │ │ ├── enemy.png │ │ ├── gopher.png │ │ └── game.go │ └── main.go ├── flappygopher │ ├── flappygopher │ │ ├── sky.png │ │ ├── wall.png │ │ ├── gopher.png │ │ └── game.go │ └── main.go ├── jumpingopher │ ├── jumpingopher │ │ ├── cloud.png │ │ ├── sky.png │ │ ├── gopher.png │ │ ├── gopher_r.png │ │ ├── platform.png │ │ └── game.go │ ├── main.go │ └── README.md ├── blocks │ ├── main.go │ └── blocks │ │ └── blocks.go ├── all │ ├── main.go │ └── all │ │ └── all.go └── snakegame │ ├── main.go │ └── snakegame │ └── snakegame.go ├── bench.go ├── error.go ├── targets ├── conf2025badge.json ├── zero-kb02.json ├── gopher-board-i2c.json └── gopher-board-spi.json ├── hardware.go ├── go.mod ├── util_test.go ├── go.sum ├── .github └── workflows │ ├── ci.yml │ └── static.yml ├── LICENSE.txt ├── util.go ├── Makefile ├── README.md ├── hardware ├── wasm.go ├── macropad-rp2040.go ├── gopher-board-i2c.go ├── pybadge.go ├── gopher-badge.go ├── gopher-board-spi.go ├── wioterminal.go ├── conf2025badge.go └── zero-kb02.go ├── game.go ├── image.go ├── inpututil.go ├── geom.go └── koebiten.go /static/ogp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sago35/koebiten/HEAD/static/ogp.png -------------------------------------------------------------------------------- /images/flappygopher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sago35/koebiten/HEAD/images/flappygopher.jpg -------------------------------------------------------------------------------- /images/jumpingopher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sago35/koebiten/HEAD/images/jumpingopher.jpg -------------------------------------------------------------------------------- /examples/GeoM/geom/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sago35/koebiten/HEAD/examples/GeoM/geom/gopher.png -------------------------------------------------------------------------------- /images/flappygopher_miniten.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sago35/koebiten/HEAD/images/flappygopher_miniten.png -------------------------------------------------------------------------------- /games/goradius/goradius/beam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sago35/koebiten/HEAD/games/goradius/goradius/beam.png -------------------------------------------------------------------------------- /games/goradius/goradius/enemy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sago35/koebiten/HEAD/games/goradius/goradius/enemy.png -------------------------------------------------------------------------------- /games/goradius/goradius/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sago35/koebiten/HEAD/games/goradius/goradius/gopher.png -------------------------------------------------------------------------------- /bench.go: -------------------------------------------------------------------------------- 1 | //go:build koebiten_benchmark 2 | 3 | package koebiten 4 | 5 | func init() { 6 | enableBenchmark = true 7 | } 8 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package koebiten 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var Termination = errors.New("Regular termination") 8 | -------------------------------------------------------------------------------- /games/flappygopher/flappygopher/sky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sago35/koebiten/HEAD/games/flappygopher/flappygopher/sky.png -------------------------------------------------------------------------------- /games/flappygopher/flappygopher/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sago35/koebiten/HEAD/games/flappygopher/flappygopher/wall.png -------------------------------------------------------------------------------- /games/jumpingopher/jumpingopher/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sago35/koebiten/HEAD/games/jumpingopher/jumpingopher/cloud.png -------------------------------------------------------------------------------- /games/jumpingopher/jumpingopher/sky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sago35/koebiten/HEAD/games/jumpingopher/jumpingopher/sky.png -------------------------------------------------------------------------------- /games/flappygopher/flappygopher/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sago35/koebiten/HEAD/games/flappygopher/flappygopher/gopher.png -------------------------------------------------------------------------------- /games/jumpingopher/jumpingopher/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sago35/koebiten/HEAD/games/jumpingopher/jumpingopher/gopher.png -------------------------------------------------------------------------------- /targets/conf2025badge.json: -------------------------------------------------------------------------------- 1 | { 2 | "inherits": [ 3 | "xiao-rp2040" 4 | ], 5 | "build-tags": ["conf2025badge"] 6 | } 7 | -------------------------------------------------------------------------------- /games/jumpingopher/jumpingopher/gopher_r.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sago35/koebiten/HEAD/games/jumpingopher/jumpingopher/gopher_r.png -------------------------------------------------------------------------------- /games/jumpingopher/jumpingopher/platform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sago35/koebiten/HEAD/games/jumpingopher/jumpingopher/platform.png -------------------------------------------------------------------------------- /hardware.go: -------------------------------------------------------------------------------- 1 | package koebiten 2 | 3 | type Hardware interface { 4 | Init() error 5 | GetDisplay() Displayer 6 | KeyUpdate() error 7 | } 8 | -------------------------------------------------------------------------------- /targets/zero-kb02.json: -------------------------------------------------------------------------------- 1 | { 2 | "inherits": [ 3 | "waveshare-rp2040-zero" 4 | ], 5 | "build-tags": ["zero_kb02"] 6 | } 7 | 8 | -------------------------------------------------------------------------------- /targets/gopher-board-i2c.json: -------------------------------------------------------------------------------- 1 | { 2 | "inherits": [ 3 | "waveshare-rp2040-zero" 4 | ], 5 | "build-tags": ["gopher_board_i2c"] 6 | } 7 | 8 | -------------------------------------------------------------------------------- /targets/gopher-board-spi.json: -------------------------------------------------------------------------------- 1 | { 2 | "inherits": [ 3 | "waveshare-rp2040-zero" 4 | ], 5 | "build-tags": ["gopher_board_spi"] 6 | } 7 | 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sago35/koebiten 2 | 3 | go 1.22.5 4 | 5 | require ( 6 | github.com/chewxy/math32 v1.11.1 7 | tinygo.org/x/drivers v0.33.0 8 | tinygo.org/x/tinydraw v0.4.0 9 | tinygo.org/x/tinyfont v0.6.0 10 | ) 11 | 12 | require github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 13 | -------------------------------------------------------------------------------- /examples/GeoM/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/sago35/koebiten" 7 | 8 | "github.com/sago35/koebiten/examples/GeoM/geom" 9 | "github.com/sago35/koebiten/hardware" 10 | ) 11 | 12 | func main() { 13 | koebiten.SetHardware(hardware.Device) 14 | koebiten.SetWindowSize(128, 64) 15 | koebiten.SetWindowTitle("GeoM Gopher") 16 | 17 | game := geom.NewGame() 18 | 19 | if err := koebiten.RunGame(game); err != nil { 20 | log.Fatal(err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/drawing/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/sago35/koebiten" 7 | "github.com/sago35/koebiten/examples/drawing/drawing" 8 | "github.com/sago35/koebiten/hardware" 9 | ) 10 | 11 | func main() { 12 | koebiten.SetHardware(hardware.Device) 13 | koebiten.SetWindowSize(128, 64) 14 | koebiten.SetWindowTitle("Drawing") 15 | 16 | game := drawing.NewGame() 17 | 18 | if err := koebiten.RunGame(game); err != nil { 19 | log.Fatal(err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /games/goradius/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/sago35/koebiten" 7 | 8 | "github.com/sago35/koebiten/games/goradius/goradius" 9 | "github.com/sago35/koebiten/hardware" 10 | ) 11 | 12 | func main() { 13 | koebiten.SetHardware(hardware.Device) 14 | koebiten.SetWindowSize(128, 64) 15 | koebiten.SetWindowTitle("GeoM Gopher") 16 | 17 | game := goradius.NewGame() 18 | 19 | if err := koebiten.RunGame(game); err != nil { 20 | log.Fatal(err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /games/jumpingopher/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/sago35/koebiten" 7 | "github.com/sago35/koebiten/games/jumpingopher/jumpingopher" 8 | "github.com/sago35/koebiten/hardware" 9 | ) 10 | 11 | func main() { 12 | koebiten.SetHardware(hardware.Device) 13 | koebiten.SetWindowSize(128, 64) 14 | koebiten.SetWindowTitle("Jumpin Gopher") 15 | game := jumpingopher.NewGame() 16 | 17 | if err := koebiten.RunGame(game); err != nil { 18 | log.Fatal(err) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /games/flappygopher/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/sago35/koebiten" 7 | "github.com/sago35/koebiten/games/flappygopher/flappygopher" 8 | "github.com/sago35/koebiten/hardware" 9 | ) 10 | 11 | func main() { 12 | koebiten.SetHardware(hardware.Device) 13 | koebiten.SetWindowSize(128, 64) 14 | koebiten.SetWindowTitle("Flappy Gopher") 15 | 16 | game := flappygopher.NewGame() 17 | 18 | if err := koebiten.RunGame(game); err != nil { 19 | log.Fatal(err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /games/blocks/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/sago35/koebiten" 7 | "github.com/sago35/koebiten/games/blocks/blocks" 8 | "github.com/sago35/koebiten/hardware" 9 | ) 10 | 11 | func main() { 12 | koebiten.SetHardware(hardware.Device) 13 | koebiten.SetRotation(koebiten.Rotation90) 14 | koebiten.SetWindowSize(64, 128) 15 | koebiten.SetWindowTitle("Tetris in Go") 16 | 17 | game := blocks.NewGame() 18 | 19 | if err := koebiten.RunGame(game); err != nil { 20 | log.Fatal(err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /games/all/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/sago35/koebiten" 7 | "github.com/sago35/koebiten/games/all/all" 8 | "github.com/sago35/koebiten/hardware" 9 | ) 10 | 11 | func main() { 12 | koebiten.SetHardware(hardware.Device) 13 | koebiten.SetWindowSize(64, 128) 14 | koebiten.SetWindowTitle("All") 15 | 16 | game := all.NewGame() 17 | 18 | for { 19 | koebiten.SetRotation(0) 20 | if err := koebiten.RunGame(game); err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | game.RunCurrentGame() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /games/jumpingopher/README.md: -------------------------------------------------------------------------------- 1 | ### `jumpingopher` の説明 2 | 3 | `jumpingopher` は、TinyGo と koebiten を使用して作成されたゲームです。 4 | 5 | ゲームの基本的な概要は以下の通りです: 6 | 7 | - **操作方法:** クリックでキャラクター(ゴーファーくん)をジャンプさせる。 8 | - **連続ジャンプ:** 連続でジャンプも可能。 9 | - **制限事項:** キャラクターが枠外に飛び出すとゲームオーバーになります。 10 | - **目的:** 流れてくる雲をジャンプで避けて、高得点を目指す。 11 | 12 | #### ゲームの特徴 13 | 14 | 1. **シンプルな操作性:** 15 | - クリック一つでキャラクターをジャンプさせることができるため、直感的なプレイが可能です。 16 | 17 | 2. **チャレンジングな要素:** 18 | - 継続してジャンプを決めることで高得点を狙える一方、枠外に出てしまうと即ゲームオーバーとなるスリリングな要素があります。 19 | 20 | 3. **グラフィックx:** 21 | - TinyGo を用いて軽量でありながら、楽しいグラフィックが楽しめます。 22 | 23 | 4. **リプレイ性:** 24 | - 高得点を目指すための繰り返しプレイが促進される設計になっています。 25 | -------------------------------------------------------------------------------- /games/snakegame/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/sago35/koebiten" 8 | "github.com/sago35/koebiten/games/snakegame/snakegame" 9 | "github.com/sago35/koebiten/hardware" 10 | "tinygo.org/x/drivers/pixel" 11 | ) 12 | 13 | var ( 14 | white = pixel.NewMonochrome(0xFF, 0xFF, 0xFF) 15 | black = pixel.NewMonochrome(0x00, 0x00, 0x00) 16 | 17 | gridSize = 4 18 | width = 128 / gridSize 19 | height = 64 / gridSize 20 | initialSpeed = 100 * time.Millisecond 21 | ) 22 | 23 | func main() { 24 | rand.Seed(time.Now().UnixNano()) 25 | koebiten.SetHardware(hardware.Device) 26 | koebiten.SetWindowSize(128, 64) 27 | koebiten.SetWindowTitle("Snake Game") 28 | 29 | game := snakegame.NewGame() 30 | koebiten.RunGame(game) 31 | } 32 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package koebiten 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | ) 7 | 8 | func TestRGBATo565(t *testing.T) { 9 | c := color.RGBA{0xFF, 0xFF, 0xFF, 0xFF} 10 | c5 := RGBATo565(c) 11 | if g, e := c5, uint16(0xFFFF); g != e { 12 | t.Errorf("got %04X want %04X", g, e) 13 | } 14 | } 15 | 16 | func TestC565toRGBA(t *testing.T) { 17 | c := uint16(0xFFFF) 18 | rgba := C565toRGBA(c) 19 | if g, e := rgba.R, uint8(0xF8); g != e { 20 | t.Errorf("got %04X want %04X", g, e) 21 | } 22 | if g, e := rgba.G, uint8(0xFC); g != e { 23 | t.Errorf("got %04X want %04X", g, e) 24 | } 25 | if g, e := rgba.B, uint8(0xF8); g != e { 26 | t.Errorf("got %04X want %04X", g, e) 27 | } 28 | if g, e := rgba.A, uint8(0xFF); g != e { 29 | t.Errorf("got %04X want %04X", g, e) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chewxy/math32 v1.11.1 h1:b7PGHlp8KjylDoU8RrcEsRuGZhJuz8haxnKfuMMRqy8= 2 | github.com/chewxy/math32 v1.11.1/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs= 3 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 4 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 5 | tinygo.org/x/drivers v0.33.0 h1:5r8Ab0IxjWQi7LzYLNWpya6U4nedo9ZtxeMaAzrJTG8= 6 | tinygo.org/x/drivers v0.33.0/go.mod h1:ZdErNrApSABdVXjA1RejD67R8SNRI6RKVfYgQDZtKtk= 7 | tinygo.org/x/tinydraw v0.4.0 h1:U9V0mHz8/jPShKjlh199vCfq1ARFyUOD1b+FfqIwV8c= 8 | tinygo.org/x/tinydraw v0.4.0/go.mod h1:WCV/EMljTv8w04iAxjv+fRD6/4ffx0afATYeJlN90Yo= 9 | tinygo.org/x/tinyfont v0.6.0 h1:GibXDSFz6xrWnEDkDRo6vsbOyRw0MVj/eza3zNHMSHs= 10 | tinygo.org/x/tinyfont v0.6.0/go.mod h1:onflMSkpWl7r7j4MIqhPEVV39pn7yL4N3MOePl3G+G8= 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | smoketest: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: smoketest 17 | run: | 18 | docker pull tinygo/tinygo-dev 19 | docker run --rm -v $GITHUB_WORKSPACE:/workspace -w /workspace tinygo/tinygo-dev tinygo version 20 | docker run --rm -v $GITHUB_WORKSPACE:/workspace -w /workspace tinygo/tinygo-dev /bin/sh -c "\ 21 | apt-get update && \ 22 | apt-get install -y libasound2-dev libgl1-mesa-dev libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev libxxf86vm-dev && \ 23 | git config --global --add safe.directory /workspace && \ 24 | make smoketest" 25 | 26 | - name: Upload uf2 files 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: uf2-files 30 | path: ./out/*.uf2 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2024 sago35 2 | 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: 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 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. 5 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | name: Deploy static wasm 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: "pages" 15 | cancel-in-progress: false 16 | 17 | jobs: 18 | deploy: 19 | environment: 20 | name: github-pages 21 | url: ${{ steps.deployment.outputs.page_url }} 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup TinyGo 28 | uses: acifani/setup-tinygo@v2 29 | with: 30 | tinygo-version: '0.36.0' 31 | 32 | - name: Build WASM binary 33 | run: | 34 | tinygo build -o ./static/main.wasm -target wasm --no-debug --panic trap --tags koebiten_benchmark ./games/all 35 | cp $(tinygo env TINYGOROOT)/targets/wasm_exec.js ./static/ 36 | 37 | - name: Setup Pages 38 | uses: actions/configure-pages@v5 39 | 40 | - name: Upload artifact 41 | uses: actions/upload-pages-artifact@v3 42 | with: 43 | path: ./static 44 | 45 | - name: Deploy to GitHub Pages 46 | id: deployment 47 | uses: actions/deploy-pages@v4 48 | -------------------------------------------------------------------------------- /static/main.js: -------------------------------------------------------------------------------- 1 | import ScreenEmulator from "./screen-emulator.js"; 2 | 3 | const screenContainer = document.querySelector('.screen-container'); 4 | let screen = new ScreenEmulator(128, 64, 5); 5 | screenContainer.appendChild(screen.canvas); 6 | 7 | let keysPressed = {}; // 押されているキーを記録 8 | 9 | async function loadWASM() { 10 | const go = new Go(); 11 | const wasmModule = await WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject); 12 | go.run(wasmModule.instance); 13 | 14 | requestAnimationFrame(processKeys); // キー入力処理を開始 15 | } 16 | 17 | // **キーが押されたときに記録** 18 | document.addEventListener("keydown", (event) => { 19 | keysPressed[event.key] = true; 20 | }); 21 | 22 | // **キーが離されたときに削除** 23 | document.addEventListener("keyup", (event) => { 24 | delete keysPressed[event.key]; 25 | }); 26 | 27 | // **キーを処理するループ** 28 | function processKeys() { 29 | if (window.wasmKeyEvent) { 30 | for (const key in keysPressed) { 31 | if (keysPressed[key]) { 32 | window.wasmKeyEvent(key); // すべての押されているキーを送信 33 | } 34 | } 35 | } 36 | requestAnimationFrame(processKeys); // 次のフレームもチェック 37 | } 38 | 39 | screen.canvas.addEventListener("touchstart", (event) => { 40 | event.preventDefault(); 41 | 42 | if (window.wasmKeyEvent) { 43 | window.wasmKeyEvent("0"); 44 | } 45 | }); 46 | 47 | window.setPixel = (x, y, r, g, b, a) => { 48 | screen.setPixel(x, y, { r, g, b, a }); 49 | }; 50 | 51 | window.display = () => { 52 | screen.display(); 53 | }; 54 | 55 | window.clearScreen = () => { 56 | screen.buffer.fill(0); 57 | }; 58 | 59 | loadWASM(); 60 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package koebiten 2 | 3 | import ( 4 | "image/color" 5 | ) 6 | 7 | // RGBATo565 converts a color.RGBA to uint16 8 | func RGBATo565(c color.RGBA) uint16 { 9 | r, g, b, _ := c.RGBA() 10 | return uint16((r & 0xF800) + 11 | ((g & 0xFC00) >> 5) + 12 | ((b & 0xF800) >> 11)) 13 | } 14 | 15 | // C565toRGBA converts a uint16 color to color.RGBA 16 | func C565toRGBA(c uint16) color.RGBA { 17 | r := ((c & 0xF800) >> 11) << 3 18 | g := ((c & 0x07E0) >> 5) << 2 19 | b := ((c & 0x001F) >> 0) << 3 20 | return color.RGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 0xFF} 21 | } 22 | 23 | type RotatedDisplay struct { 24 | Displayer 25 | mode int 26 | } 27 | 28 | func (d *RotatedDisplay) Size() (x, y int16) { 29 | switch d.mode { 30 | case 0, 2: 31 | return x, y 32 | default: 33 | } 34 | return y, x 35 | } 36 | 37 | func (d *RotatedDisplay) SetPixel(x, y int16, c color.RGBA) { 38 | switch d.mode { 39 | case Rotation0: 40 | d.Displayer.SetPixel(x, y, c) 41 | case Rotation90: 42 | sx, _ := d.Displayer.Size() 43 | d.Displayer.SetPixel(sx-y, x, c) 44 | case Rotation180: 45 | sx, sy := d.Displayer.Size() 46 | d.Displayer.SetPixel(sx-x, sy-y, c) 47 | case Rotation270: 48 | _, sy := d.Displayer.Size() 49 | d.Displayer.SetPixel(y, sy-x, c) 50 | } 51 | } 52 | 53 | // Clockwise rotation of the screen. 54 | const ( 55 | Rotation0 = iota 56 | Rotation90 57 | Rotation180 58 | Rotation270 59 | ) 60 | 61 | // dummyGame is a type that represents a simple game function. 62 | type dummyGame func() 63 | 64 | func (g dummyGame) Update() error { 65 | return nil 66 | } 67 | 68 | func (g dummyGame) Draw(*Image) { 69 | g() 70 | } 71 | 72 | func (g dummyGame) Layout(int, int) (int, int) { 73 | return 128, 64 74 | } 75 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | smoketest: FORCE 2 | mkdir -p out 3 | tinygo build -o ./out/all.zero-kb02.uf2 --size short --target ./targets/zero-kb02.json ./games/all/ 4 | tinygo build -o ./out/flappygopher.zero-kb02.uf2 --size short --target ./targets/zero-kb02.json ./games/flappygopher/ 5 | tinygo build -o ./out/jumpingopher.zero-kb02.uf2 --size short --target ./targets/zero-kb02.json ./games/jumpingopher/ 6 | tinygo build -o ./out/blocks.zero-kb02.uf2 --size short --target ./targets/zero-kb02.json ./games/blocks/ 7 | tinygo build -o ./out/example_GeoM.zero-kb02.uf2 --size short --target ./targets/zero-kb02.json ./examples/GeoM/ 8 | tinygo build -o ./out/example_drawing.zero-kb02.uf2 --size short --target ./targets/zero-kb02.json ./examples/drawing/ 9 | tinygo build -o ./out/all.gopher-badge.uf2 --size short --target gopher-badge ./games/all/ 10 | tinygo build -o ./out/all.pybadge.uf2 --size short --target pybadge ./games/all/ 11 | tinygo build -o ./out/all.wioterminal.uf2 --size short --target wioterminal ./games/all/ 12 | tinygo build -o ./out/all.macropad-rp2040.uf2 --size short --target macropad-rp2040 ./games/all/ 13 | tinygo build -o ./out/all.gopher-board-i2c.uf2 --size short --target ./targets/gopher-board-i2c.json ./games/all/ 14 | tinygo build -o ./out/all.gopher-board-spi.uf2 --size short --target ./targets/gopher-board-spi.json ./games/all/ 15 | tinygo build -o ./out/all.conf2025badge.uf2 --size short --target ./targets/conf2025badge.json ./games/all/ 16 | tinygo build -o ./out/all.wasm --size short --target wasm --no-debug --panic trap ./games/all/ 17 | tinygo build -o ./out/goradius.zero-kb02.uf2 --size short --target ./targets/zero-kb02.json ./games/goradius/ 18 | 19 | FORCE: 20 | -------------------------------------------------------------------------------- /static/screen-emulator.js: -------------------------------------------------------------------------------- 1 | export default class ScreenEmulator { 2 | constructor(width, height, scale = 1) { 3 | this.width = width; 4 | this.height = height; 5 | this.scale = scale; 6 | this.buffer = new Uint32Array(width * height); // RGBA 格納用 7 | 8 | // 外枠用の div を作成 9 | this.container = document.createElement("div"); 10 | this.container.style.display = "inline-block"; 11 | this.container.style.border = "4px solid black"; // 外枠 12 | this.container.style.padding = "10px"; // キャンバスとの間隔 13 | this.container.style.backgroundColor = "#222"; // 背景色(黒に近い) 14 | this.container.style.boxShadow = "0 0 10px rgba(0, 0, 0, 0.5)"; // 影を追加 15 | 16 | // Canvas 作成 17 | this.canvas = document.createElement("canvas"); 18 | this.ctx = this.canvas.getContext("2d"); 19 | 20 | this.canvas.width = width * scale; 21 | this.canvas.height = height * scale; 22 | this.ctx.imageSmoothingEnabled = false; // ピクセルを綺麗に保つ 23 | this.canvas.style.display = "block"; 24 | 25 | // DOM に追加 26 | this.container.appendChild(this.canvas); 27 | } 28 | 29 | size() { 30 | return { x: this.width, y: this.height }; 31 | } 32 | 33 | setPixel(x, y, { r, g, b, a }) { 34 | if (x < 0 || x >= this.width || y < 0 || y >= this.height) return; 35 | const index = y * this.width + x; 36 | this.buffer[index] = (a << 24) | (r << 16) | (g << 8) | b; 37 | } 38 | 39 | display() { 40 | const imageData = this.ctx.createImageData(this.width, this.height); 41 | const data = new Uint32Array(imageData.data.buffer); 42 | 43 | for (let i = 0; i < this.buffer.length; i++) { 44 | data[i] = this.buffer[i]; 45 | } 46 | 47 | // 小さいキャンバスに描画し、拡大して表示 48 | const tempCanvas = document.createElement("canvas"); 49 | tempCanvas.width = this.width; 50 | tempCanvas.height = this.height; 51 | const tempCtx = tempCanvas.getContext("2d"); 52 | tempCtx.putImageData(imageData, 0, 0); 53 | 54 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 55 | this.ctx.drawImage(tempCanvas, 0, 0, this.canvas.width, this.canvas.height); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/drawing/drawing/drawing.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "github.com/sago35/koebiten" 5 | "tinygo.org/x/drivers/pixel" 6 | ) 7 | 8 | var ( 9 | white = pixel.NewMonochrome(0xFF, 0xFF, 0xFF) 10 | black = pixel.NewMonochrome(0x00, 0x00, 0x00) 11 | ) 12 | 13 | type Pointer struct { 14 | x, y int 15 | } 16 | 17 | type Game struct { 18 | pointer Pointer 19 | thick int 20 | canvas *koebiten.Image 21 | } 22 | 23 | func NewGame() *Game { 24 | return &Game{ 25 | pointer: Pointer{64, 32}, 26 | canvas: koebiten.NewImage(128, 64), 27 | } 28 | } 29 | 30 | func (g *Game) Update() error { 31 | if koebiten.IsKeyPressed(koebiten.KeyArrowLeft) { 32 | g.pointer.x-- 33 | } 34 | if koebiten.IsKeyPressed(koebiten.KeyArrowRight) { 35 | g.pointer.x++ 36 | } 37 | if koebiten.IsKeyPressed(koebiten.KeyArrowUp) { 38 | g.pointer.y-- 39 | } 40 | if koebiten.IsKeyPressed(koebiten.KeyArrowDown) { 41 | g.pointer.y++ 42 | } 43 | 44 | if koebiten.IsKeyPressed(koebiten.KeyRotaryLeft) { 45 | g.thick-- 46 | if g.thick < 0 { 47 | g.thick = 0 48 | } 49 | } 50 | if koebiten.IsKeyPressed(koebiten.KeyRotaryRight) { 51 | g.thick++ 52 | if g.thick > 10 { 53 | g.thick = 10 54 | } 55 | } 56 | 57 | if koebiten.IsKeyPressed(koebiten.Key0) { 58 | g.draw(g.canvas, g.pointer.x, g.pointer.y, black) 59 | } 60 | if koebiten.IsKeyPressed(koebiten.Key1) || koebiten.IsKeyPressed(koebiten.Key2) || 61 | koebiten.IsKeyPressed(koebiten.Key3) || koebiten.IsKeyPressed(koebiten.Key4) || 62 | koebiten.IsKeyPressed(koebiten.Key5) || koebiten.IsKeyPressed(koebiten.Key6) || 63 | koebiten.IsKeyPressed(koebiten.Key7) || koebiten.IsKeyPressed(koebiten.Key8) || 64 | koebiten.IsKeyPressed(koebiten.Key9) || koebiten.IsKeyPressed(koebiten.Key10) || 65 | koebiten.IsKeyPressed(koebiten.Key11) { 66 | g.draw(g.canvas, g.pointer.x, g.pointer.y, white) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func (g *Game) Draw(screen *koebiten.Image) { 73 | g.canvas.DrawImage(screen, koebiten.DrawImageOptions{}) 74 | koebiten.DrawFilledCircle(screen, g.pointer.x, g.pointer.y, g.thick+1, white) 75 | } 76 | 77 | func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { 78 | return 128, 64 79 | } 80 | 81 | func (g *Game) draw(canvas *koebiten.Image, x, y int, color pixel.BaseColor) { 82 | koebiten.DrawFilledCircle(g.canvas, x, y, g.thick, color) 83 | } 84 | 85 | func isAnyKeyPressed() bool { 86 | keys := []koebiten.Key{ 87 | koebiten.Key0, koebiten.Key1, koebiten.Key2, koebiten.Key3, 88 | koebiten.Key4, koebiten.Key5, koebiten.Key6, koebiten.Key7, 89 | koebiten.Key8, koebiten.Key9, koebiten.Key10, koebiten.Key11, 90 | } 91 | for _, key := range keys { 92 | if koebiten.IsKeyPressed(key) { 93 | return true 94 | } 95 | } 96 | return false 97 | } 98 | -------------------------------------------------------------------------------- /examples/GeoM/geom/game.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import ( 4 | "embed" 5 | "slices" 6 | 7 | "github.com/sago35/koebiten" 8 | "tinygo.org/x/drivers/pixel" 9 | ) 10 | 11 | //go:embed *.png 12 | var fsys embed.FS 13 | 14 | const ( 15 | width = 128 16 | height = 64 17 | 18 | gopherWidth = 20 19 | gopherHeight = 25 20 | ) 21 | 22 | type Game struct { 23 | gopher *koebiten.Image 24 | x, y int 25 | scale float32 26 | theta float32 27 | } 28 | 29 | var ( 30 | white = pixel.NewMonochrome(0xFF, 0xFF, 0xFF) 31 | black = pixel.NewMonochrome(0x00, 0x00, 0x00) 32 | ) 33 | 34 | func NewGame() *Game { 35 | game := &Game{ 36 | gopher: koebiten.NewImageFromFS(fsys, "gopher.png"), 37 | x: width / 2, 38 | y: height / 2, 39 | scale: 1, 40 | } 41 | return game 42 | } 43 | 44 | // Game update process 45 | func (g *Game) Update() error { 46 | ds := float32(0.05) 47 | dt := float32(0.2) 48 | dx := 1 49 | dy := 1 50 | 51 | // rotary buttonを回すとgopherが回転する 52 | // キーボードを押しながら回すと拡大縮小する 53 | if koebiten.IsKeyPressed(koebiten.KeyRotaryRight) { 54 | if isAnyKeyboardKeyPressed() { 55 | g.scale += ds 56 | } else { 57 | g.theta += dt 58 | } 59 | } 60 | if koebiten.IsKeyPressed(koebiten.KeyRotaryLeft) { 61 | if isAnyKeyboardKeyPressed() { 62 | g.scale -= ds 63 | } else { 64 | g.theta -= dt 65 | } 66 | } 67 | 68 | // joystickを倒すとgopherが移動する 69 | if koebiten.IsKeyPressed(koebiten.KeyArrowRight) { 70 | g.x += dx 71 | } 72 | if koebiten.IsKeyPressed(koebiten.KeyArrowLeft) { 73 | g.x -= dx 74 | } 75 | if koebiten.IsKeyPressed(koebiten.KeyArrowDown) { 76 | g.y += dy 77 | } 78 | if koebiten.IsKeyPressed(koebiten.KeyArrowUp) { 79 | g.y -= dy 80 | } 81 | 82 | return nil 83 | } 84 | 85 | // Screen size 86 | func (g *Game) Layout(outsideWidth, outsideHeight int) (w, h int) { 87 | return width, height 88 | } 89 | 90 | func (g *Game) Draw(screen *koebiten.Image) { 91 | op := koebiten.DrawImageOptions{} 92 | op.GeoM.Translate(-float32(gopherWidth)/2, -float32(gopherHeight)/2) 93 | op.GeoM.Scale(g.scale, g.scale) 94 | op.GeoM.Rotate(g.theta) 95 | op.GeoM.Translate(float32(g.x), float32(g.y)) 96 | g.gopher.DrawImage(screen, op) 97 | } 98 | 99 | // isAnyKeyboardKeyPressed returns true if any keyboard key is pressed 100 | // 101 | // keyboard key are koebiten.Key0 to koebiten.Key11 102 | func isAnyKeyboardKeyPressed() bool { 103 | return slices.ContainsFunc(koebiten.AppendPressedKeys(nil), func(k koebiten.Key) bool { 104 | switch k { 105 | case 106 | koebiten.Key0, 107 | koebiten.Key1, 108 | koebiten.Key2, 109 | koebiten.Key3, 110 | koebiten.Key4, 111 | koebiten.Key5, 112 | koebiten.Key6, 113 | koebiten.Key7, 114 | koebiten.Key8, 115 | koebiten.Key9, 116 | koebiten.Key10, 117 | koebiten.Key11: 118 | return true 119 | default: 120 | return false 121 | } 122 | }) 123 | } 124 | -------------------------------------------------------------------------------- /games/all/all/all.go: -------------------------------------------------------------------------------- 1 | package all 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/sago35/koebiten" 7 | "github.com/sago35/koebiten/games/blocks/blocks" 8 | "github.com/sago35/koebiten/games/flappygopher/flappygopher" 9 | "github.com/sago35/koebiten/games/goradius/goradius" 10 | "github.com/sago35/koebiten/games/jumpingopher/jumpingopher" 11 | "github.com/sago35/koebiten/games/snakegame/snakegame" 12 | ) 13 | 14 | type Game struct { 15 | Title string 16 | Game func() 17 | } 18 | 19 | type Menu struct { 20 | index int 21 | games []Game 22 | } 23 | 24 | func NewGame() *Menu { 25 | menu := &Menu{ 26 | index: 0, 27 | } 28 | 29 | menu.AddGames([]Game{ 30 | { 31 | Title: "Flappy Gopher", 32 | Game: func() { 33 | koebiten.SetRotation(koebiten.Rotation0) 34 | game := flappygopher.NewGame() 35 | if err := koebiten.RunGame(game); err != nil { 36 | log.Fatal(err) 37 | } 38 | }, 39 | }, 40 | { 41 | Title: "Blocks", 42 | Game: func() { 43 | koebiten.SetRotation(koebiten.Rotation90) 44 | game := blocks.NewGame() 45 | if err := koebiten.RunGame(game); err != nil { 46 | log.Fatal(err) 47 | } 48 | }, 49 | }, 50 | { 51 | Title: "Jumpin Gopher", 52 | Game: func() { 53 | koebiten.SetRotation(koebiten.Rotation0) 54 | game := jumpingopher.NewGame() 55 | if err := koebiten.RunGame(game); err != nil { 56 | log.Fatal(err) 57 | } 58 | }, 59 | }, 60 | { 61 | Title: "Snake Game", 62 | Game: func() { 63 | koebiten.SetRotation(koebiten.Rotation0) 64 | game := snakegame.NewGame() 65 | if err := koebiten.RunGame(game); err != nil { 66 | log.Fatal(err) 67 | } 68 | }, 69 | }, 70 | { 71 | Title: "Goradius", 72 | Game: func() { 73 | koebiten.SetRotation(koebiten.Rotation0) 74 | game := goradius.NewGame() 75 | if err := koebiten.RunGame(game); err != nil { 76 | log.Fatal(err) 77 | } 78 | }, 79 | }, 80 | }) 81 | 82 | return menu 83 | } 84 | 85 | func (m *Menu) Update() error { 86 | if koebiten.IsKeyJustPressed(koebiten.KeyDown) || koebiten.IsKeyJustPressed(koebiten.KeyRotaryRight) { 87 | m.index = (m.index + 1) % len(m.games) 88 | } else if koebiten.IsKeyJustPressed(koebiten.KeyUp) || koebiten.IsKeyJustPressed(koebiten.KeyRotaryLeft) { 89 | m.index = (m.index - 1 + len(m.games)) % len(m.games) 90 | } else if len(koebiten.AppendJustPressedKeys(nil)) > 0 { 91 | return koebiten.Termination 92 | } 93 | return nil 94 | } 95 | 96 | // Screen size 97 | func (m *Menu) Layout(outsideWidth, outsideHeight int) (w, h int) { 98 | return 128, 64 99 | } 100 | 101 | func (m *Menu) Draw(screen *koebiten.Image) { 102 | koebiten.Println("select game :") 103 | koebiten.Println(m.games[m.index].Title) 104 | } 105 | 106 | func (m *Menu) AddGames(game []Game) { 107 | m.games = append(m.games, game...) 108 | } 109 | 110 | func (m *Menu) RunCurrentGame() { 111 | m.games[m.index].Game() 112 | } 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # koebiten 2 | 3 | **The API is not yet fixed.** 4 | 5 | Koebiten is a package for making simple games. 6 | Koebiten was heavily influenced by a package called Miniten when development began. 7 | Since then, additional features have been inspired by Miniten's foundation, Ebitengine. 8 | 9 | * https://ebitengine.org/ 10 | * https://github.com/eihigh/miniten 11 | 12 | ## Demo 13 | 14 | You can try the Wasm version of `koebiten` from your browser. 15 | 16 | https://sago35.github.io/koebiten/ 17 | 18 | ## games/flappygopher 19 | 20 | ![](./images/flappygopher.jpg) 21 | 22 | For now, koebiten only works on [zero-kb02](https://github.com/sago35/keyboards) and [macropad-rp2040](https://learn.adafruit.com/adafruit-macropad-rp2040). It needs some improvements to run in a more general environment. 23 | 24 | ``` 25 | $ tinygo flash --target ./targets/zero-kb02.json --size short ./games/flappygopher 26 | $ tinygo flash --target gopher-badge --size short ./games/flappygopher 27 | ``` 28 | 29 | ## games/jumpingopher 30 | 31 | ![](./images/jumpingopher.jpg) 32 | 33 | ``` 34 | $ tinygo flash --target ./targets/zero-kb02.json --size short ./games/jumpingopher 35 | $ tinygo flash --target gopher-badge --size short ./games/jumpingopher 36 | ``` 37 | 38 | more info : [./games/jumpingopher](./games/jumpingopher) 39 | 40 | ## Others 41 | 42 | The other games are located under [./games](./games). 43 | 44 | Refer to the [Makefile](./Makefile) for instructions on how to build. 45 | 46 | # hardware 47 | 48 | The currently supported hardware is as follows. 49 | 50 | * zero-kb02 (--target ./targets/zero-kb02.json) 51 | * https://github.com/sago35/keyboards 52 | * gopher-badge 53 | * https://gopherbadge.com/ 54 | * pybadge / gobadge 55 | * https://learn.adafruit.com/adafruit-pybadge 56 | * wioterminal 57 | * https://www.seeedstudio.com/Wio-Terminal-p-4509.html 58 | * macropad-rp2040 59 | * https://learn.adafruit.com/adafruit-macropad-rp2040 60 | * gopher-board 61 | * https://github.com/sat0ken/gopher-board/tree/main/rp2040/v2 62 | * Wasm / WebAssembly 63 | * https://sago35.github.io/koebiten 64 | 65 | ## Add new hardware 66 | 67 | When adding new hardware, please refer to [#7](https://github.com/sago35/koebiten/pull/7). 68 | If the hardware you're adding is not a simple, off-the-shelf product like zero-kb02, 69 | you'll need a build tag such as `go:build zero_kb02`. 70 | For more details, see [#8](https://github.com/sago35/koebiten/pull/8). 71 | 72 | ## Tags 73 | 74 | ### koebiten\_benchmark 75 | 76 | If you specify the **`koebiten_benchmark`** tag, it will display the time taken to render **32 frames** every **32 frames** in **microseconds (us)**. 77 | If the processing is light enough, the displayed value should be around **1024us**. 78 | If this value becomes significantly larger, it indicates that the frame rate is unstable. 79 | 80 | ``` 81 | $ tinygo flash --target ./targets/zero-kb02.json --size short --tags koebiten_benchmark ./games/flappygopher 82 | ``` 83 | 84 | ## link 85 | 86 | * https://ebitengine.org/ 87 | * https://github.com/eihigh/miniten 88 | * https://github.com/sago35/keyboards 89 | 90 | ## The Go Gopher 91 | 92 | The Go gopher was designed by Renée French. 93 | -------------------------------------------------------------------------------- /hardware/wasm.go: -------------------------------------------------------------------------------- 1 | //go:build tinygo && wasm 2 | 3 | package hardware 4 | 5 | import ( 6 | "image/color" 7 | "syscall/js" 8 | 9 | "github.com/sago35/koebiten" 10 | ) 11 | 12 | var ( 13 | d = NewDisplay(128, 64) 14 | Device = &WasmDevice{} 15 | keyState = map[koebiten.Key]bool{} 16 | keysBuf = [1]koebiten.Key{} 17 | ) 18 | 19 | func init() { 20 | wasmKeyEvent := wasmKeyEvent() 21 | js.Global().Set("wasmKeyEvent", wasmKeyEvent) 22 | 23 | } 24 | 25 | func wasmKeyEvent() js.Func { 26 | return js.FuncOf(func(this js.Value, args []js.Value) interface{} { 27 | if len(args) < 1 { 28 | return nil 29 | } 30 | key := args[0].String() 31 | 32 | switch key { 33 | case "ArrowUp", "e", "k": 34 | keyState[koebiten.KeyArrowUp] = true 35 | case "ArrowDown", "d", "j": 36 | keyState[koebiten.KeyArrowDown] = true 37 | case "ArrowLeft", "s", "h": 38 | keyState[koebiten.KeyArrowLeft] = true 39 | case "ArrowRight", "f", "l": 40 | keyState[koebiten.KeyArrowRight] = true 41 | case "z", "n", "0", " ", "Enter": 42 | keyState[koebiten.Key0] = true 43 | case "x", "m", "1": 44 | keyState[koebiten.Key1] = true 45 | case "c", ",", "2": 46 | keyState[koebiten.Key2] = true 47 | case "v", ".", "3": 48 | keyState[koebiten.Key3] = true 49 | default: 50 | //fmt.Printf("undefined key : %q\n", key) 51 | } 52 | return nil 53 | }) 54 | } 55 | 56 | func NewDisplay(w, h int) *Display { 57 | return &Display{ 58 | w: int16(w), 59 | h: int16(h), 60 | } 61 | } 62 | 63 | type Display struct { 64 | w int16 65 | h int16 66 | } 67 | 68 | func (d *Display) Size() (x, y int16) { 69 | return d.w, d.h 70 | } 71 | 72 | func (d *Display) SetPixel(x, y int16, c color.RGBA) { 73 | js.Global().Call("setPixel", x, y, c.R, c.G, c.B, c.A) 74 | } 75 | 76 | func (d *Display) Display() error { 77 | js.Global().Call("display") 78 | return nil 79 | } 80 | 81 | func (d *Display) ClearDisplay() { 82 | js.Global().Call("clearScreen") 83 | } 84 | 85 | func (d *Display) ClearBuffer() { 86 | js.Global().Call("clearScreen") 87 | } 88 | 89 | type WasmDevice struct { 90 | } 91 | 92 | func (w *WasmDevice) GetDisplay() koebiten.Displayer { 93 | return d 94 | } 95 | 96 | func (w *WasmDevice) Init() error { 97 | return nil 98 | } 99 | 100 | func (w *WasmDevice) KeyUpdate() error { 101 | keys := []koebiten.Key{ 102 | koebiten.Key0, 103 | koebiten.Key1, 104 | koebiten.Key2, 105 | koebiten.Key3, 106 | koebiten.Key4, 107 | koebiten.Key5, 108 | koebiten.Key6, 109 | koebiten.Key7, 110 | koebiten.Key8, 111 | koebiten.Key9, 112 | koebiten.Key10, 113 | koebiten.Key11, 114 | koebiten.KeyRotaryButton, 115 | koebiten.KeyJoystick, 116 | koebiten.KeyRotaryLeft, 117 | koebiten.KeyRotaryRight, 118 | koebiten.KeyLeft, 119 | koebiten.KeyRight, 120 | koebiten.KeyUp, 121 | koebiten.KeyDown, 122 | } 123 | 124 | for _, key := range keys { 125 | keysBuf[0] = key 126 | if _, ok := keyState[key]; ok { 127 | koebiten.AppendPressedKeys(keysBuf[:]) 128 | delete(keyState, key) 129 | } else { 130 | koebiten.AppendJustReleasedKeys(keysBuf[:]) 131 | } 132 | } 133 | return nil 134 | } 135 | -------------------------------------------------------------------------------- /hardware/macropad-rp2040.go: -------------------------------------------------------------------------------- 1 | //go:build tinygo && macropad_rp2040 2 | 3 | package hardware 4 | 5 | import ( 6 | "machine" 7 | 8 | "github.com/sago35/koebiten" 9 | "tinygo.org/x/drivers/sh1106" 10 | ) 11 | 12 | var Device = &device{} 13 | 14 | type device struct { 15 | display *sh1106.Device 16 | gpioPins []machine.Pin 17 | state []State 18 | cycle []int 19 | keybuf [1]koebiten.Key 20 | } 21 | 22 | const ( 23 | debounce = 0 24 | ) 25 | 26 | type State uint8 27 | 28 | const ( 29 | None State = iota 30 | NoneToPress 31 | Press 32 | PressToRelease 33 | ) 34 | 35 | func (z *device) Init() error { 36 | err := machine.SPI1.Configure(machine.SPIConfig{ 37 | Frequency: 48000000, 38 | }) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | d := sh1106.NewSPI(machine.SPI1, machine.OLED_DC, machine.OLED_RST, machine.OLED_CS) 44 | d.Configure(sh1106.Config{ 45 | Width: 128, 46 | Height: 64, 47 | }) 48 | d.ClearDisplay() 49 | z.display = &d 50 | 51 | gpioPins := []machine.Pin{ 52 | machine.KEY1, 53 | machine.KEY2, 54 | machine.KEY3, 55 | machine.KEY4, 56 | machine.KEY5, 57 | machine.KEY6, 58 | machine.KEY7, 59 | machine.KEY8, 60 | machine.KEY9, 61 | machine.KEY10, 62 | machine.KEY11, 63 | machine.KEY12, 64 | } 65 | 66 | for i := range gpioPins { 67 | gpioPins[i].Configure(machine.PinConfig{Mode: machine.PinInputPullup}) 68 | } 69 | 70 | z.gpioPins = []machine.Pin{ 71 | machine.KEY1, 72 | machine.KEY2, 73 | machine.KEY3, 74 | machine.KEY4, 75 | machine.KEY5, 76 | machine.KEY6, 77 | machine.NoPin, 78 | machine.NoPin, 79 | machine.NoPin, 80 | machine.NoPin, 81 | machine.NoPin, 82 | machine.KEY7, 83 | machine.NoPin, 84 | machine.NoPin, 85 | machine.NoPin, 86 | machine.NoPin, 87 | machine.KEY10, 88 | machine.KEY12, 89 | machine.KEY8, 90 | machine.KEY11, 91 | } 92 | 93 | z.state = make([]State, len(z.gpioPins)) 94 | z.cycle = make([]int, len(z.gpioPins)) 95 | return nil 96 | } 97 | 98 | func (z *device) GetDisplay() koebiten.Displayer { 99 | return z.display 100 | } 101 | 102 | func (z *device) KeyUpdate() error { 103 | buf := z.keybuf[:] 104 | for r := range z.gpioPins { 105 | current := !z.gpioPins[r].Get() 106 | idx := r 107 | 108 | switch z.state[idx] { 109 | case None: 110 | if current { 111 | if z.cycle[idx] >= debounce { 112 | z.state[idx] = NoneToPress 113 | z.cycle[idx] = 0 114 | } else { 115 | z.cycle[idx]++ 116 | } 117 | } else { 118 | z.cycle[idx] = 0 119 | } 120 | case NoneToPress: 121 | z.state[idx] = Press 122 | buf[0] = koebiten.Key(idx) 123 | koebiten.AppendJustPressedKeys(buf) 124 | case Press: 125 | buf[0] = koebiten.Key(idx) 126 | koebiten.AppendPressedKeys(buf) 127 | if current { 128 | z.cycle[idx] = 0 129 | } else { 130 | if z.cycle[idx] >= debounce { 131 | z.state[idx] = PressToRelease 132 | z.cycle[idx] = 0 133 | } else { 134 | z.cycle[idx]++ 135 | } 136 | } 137 | case PressToRelease: 138 | z.state[idx] = None 139 | buf[0] = koebiten.Key(idx) 140 | koebiten.AppendJustReleasedKeys(buf) 141 | } 142 | } 143 | return nil 144 | } 145 | -------------------------------------------------------------------------------- /hardware/gopher-board-i2c.go: -------------------------------------------------------------------------------- 1 | //go:build tinygo && gopher_board_i2c 2 | 3 | package hardware 4 | 5 | import ( 6 | "machine" 7 | 8 | "github.com/sago35/koebiten" 9 | "tinygo.org/x/drivers/ssd1306" 10 | ) 11 | 12 | var ( 13 | Device = &device{} 14 | Display *ssd1306.Device 15 | gpioPins []machine.Pin 16 | ) 17 | 18 | type device struct { 19 | gpioPins []machine.Pin 20 | state []State 21 | cycle []int 22 | keybuf [1]koebiten.Key 23 | } 24 | 25 | const ( 26 | debounce = 0 27 | ) 28 | 29 | type State uint8 30 | 31 | const ( 32 | None State = iota 33 | NoneToPress 34 | Press 35 | PressToRelease 36 | ) 37 | 38 | func (z *device) GetDisplay() koebiten.Displayer { 39 | return Display 40 | } 41 | 42 | func (z *device) Init() error { 43 | i2c := machine.I2C0 44 | i2c.Configure(machine.I2CConfig{ 45 | Frequency: 2_800_000, 46 | SDA: machine.GPIO0, 47 | SCL: machine.GPIO1, 48 | }) 49 | 50 | d := ssd1306.NewI2C(i2c) 51 | d.Configure(ssd1306.Config{ 52 | Address: 0x3C, 53 | Width: 128, 54 | Height: 64, 55 | }) 56 | d.ClearDisplay() 57 | Display = d 58 | 59 | gpioPins = []machine.Pin{ 60 | machine.GPIO4, // up 61 | machine.GPIO5, // left 62 | machine.GPIO6, // down 63 | machine.GPIO7, // right 64 | machine.GPIO27, // A 65 | machine.GPIO28, // B 66 | } 67 | 68 | for _, p := range gpioPins { 69 | p.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) 70 | } 71 | 72 | z.gpioPins = []machine.Pin{ 73 | machine.GPIO27, // A 74 | machine.GPIO28, // B 75 | machine.NoPin, 76 | machine.NoPin, 77 | machine.NoPin, 78 | machine.NoPin, 79 | machine.NoPin, 80 | machine.NoPin, 81 | machine.NoPin, 82 | machine.NoPin, 83 | machine.NoPin, 84 | machine.NoPin, 85 | machine.NoPin, 86 | machine.NoPin, 87 | machine.NoPin, 88 | machine.NoPin, 89 | machine.GPIO5, // left 90 | machine.GPIO7, // right 91 | machine.GPIO4, // up 92 | machine.GPIO6, // down 93 | } 94 | 95 | z.state = make([]State, len(z.gpioPins)) 96 | z.cycle = make([]int, len(z.gpioPins)) 97 | return nil 98 | } 99 | 100 | func (z *device) KeyUpdate() error { 101 | buf := z.keybuf[:] 102 | for r := range z.gpioPins { 103 | current := !z.gpioPins[r].Get() 104 | if z.gpioPins[r] == machine.NoPin { 105 | current = false 106 | } 107 | idx := r 108 | 109 | switch z.state[idx] { 110 | case None: 111 | if current { 112 | if z.cycle[idx] >= debounce { 113 | z.state[idx] = NoneToPress 114 | z.cycle[idx] = 0 115 | } else { 116 | z.cycle[idx]++ 117 | } 118 | } else { 119 | z.cycle[idx] = 0 120 | } 121 | case NoneToPress: 122 | z.state[idx] = Press 123 | buf[0] = koebiten.Key(idx) 124 | koebiten.AppendJustPressedKeys(buf) 125 | case Press: 126 | buf[0] = koebiten.Key(idx) 127 | koebiten.AppendPressedKeys(buf) 128 | if current { 129 | z.cycle[idx] = 0 130 | } else { 131 | if z.cycle[idx] >= debounce { 132 | z.state[idx] = PressToRelease 133 | z.cycle[idx] = 0 134 | } else { 135 | z.cycle[idx]++ 136 | } 137 | } 138 | case PressToRelease: 139 | z.state[idx] = None 140 | buf[0] = koebiten.Key(idx) 141 | koebiten.AppendJustReleasedKeys(buf) 142 | } 143 | } 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /game.go: -------------------------------------------------------------------------------- 1 | package koebiten 2 | 3 | // Game defines necessary functions for a game. 4 | type Game interface { 5 | // Update updates a game by one tick. The given argument represents a screen image. 6 | // 7 | // Update updates only the game logic and Draw draws the screen. 8 | // 9 | // You can assume that Update is always called TPS-times per second (60 by default), and you can assume 10 | // that the time delta between two Updates is always 1 / TPS [s] (1/60[s] by default). As Ebitengine already 11 | // adjusts the number of Update calls, you don't have to measure time deltas in Update by e.g. OS timers. 12 | // 13 | // An actual TPS is available by ActualTPS(), and the result might slightly differ from your expected TPS, 14 | // but still, your game logic should stick to the fixed time delta and should not rely on ActualTPS() value. 15 | // This API is for just measurement and/or debugging. In the long run, the number of Update calls should be 16 | // adjusted based on the set TPS on average. 17 | // 18 | // An actual time delta between two Updates might be bigger than expected. In this case, your game's 19 | // Update or Draw takes longer than they should. In this case, there is nothing other than optimizing 20 | // your game implementation. 21 | // 22 | // In the first frame, it is ensured that Update is called at least once before Draw. You can use Update 23 | // to initialize the game state. 24 | // 25 | // After the first frame, Update might not be called or might be called once 26 | // or more for one frame. The frequency is determined by the current TPS (tick-per-second). 27 | // 28 | // If the error returned is nil, game execution proceeds normally. 29 | // If the error returned is Termination, game execution halts, but does not return an error from RunGame. 30 | // If the error returned is any other non-nil value, game execution halts and the error is returned from RunGame. 31 | Update() error 32 | 33 | // Draw draws the game screen by one frame. 34 | // 35 | // The give argument represents a screen image. The updated content is adopted as the game screen. 36 | // 37 | // The frequency of Draw calls depends on the user's environment, especially the monitors refresh rate. 38 | // For portability, you should not put your game logic in Draw in general. 39 | Draw(screen *Image) 40 | 41 | // Layout accepts a native outside size in device-independent pixels and returns the game's logical screen 42 | // size. 43 | // 44 | // On desktops, the outside is a window or a monitor (fullscreen mode). On browsers, the outside is a body 45 | // element. On mobiles, the outside is the view's size. 46 | // 47 | // Even though the outside size and the screen size differ, the rendering scale is automatically adjusted to 48 | // fit with the outside. 49 | // 50 | // Layout is called almost every frame. 51 | // 52 | // It is ensured that Layout is invoked before Update is called in the first frame. 53 | // 54 | // If Layout returns non-positive numbers, the caller can panic. 55 | // 56 | // You can return a fixed screen size if you don't care, or you can also return a calculated screen size 57 | // adjusted with the given outside size. 58 | // 59 | // If the game implements the interface LayoutFer, Layout is never called and LayoutF is called instead. 60 | Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) 61 | } 62 | -------------------------------------------------------------------------------- /games/jumpingopher/jumpingopher/game.go: -------------------------------------------------------------------------------- 1 | package jumpingopher 2 | 3 | import ( 4 | "embed" 5 | "math/rand/v2" 6 | 7 | "github.com/sago35/koebiten" 8 | ) 9 | 10 | type Game struct { 11 | } 12 | 13 | func NewGame() *Game { 14 | game := &Game{} 15 | return game 16 | } 17 | 18 | // Game update process 19 | func (g *Game) Update() error { 20 | return nil 21 | } 22 | 23 | // Screen size 24 | func (g *Game) Layout(outsideWidth, outsideHeight int) (w, h int) { 25 | return 128, 64 26 | } 27 | 28 | func (g *Game) Draw(screen *koebiten.Image) { 29 | draw() 30 | } 31 | 32 | //go:embed *.png 33 | var fsys embed.FS 34 | 35 | type cloud struct { 36 | cloudX int 37 | holeY int 38 | } 39 | 40 | type platform struct { 41 | pY int 42 | } 43 | 44 | var ( 45 | x = float32(50.0) 46 | y = float32(30.0) 47 | vy = float32(0.0) 48 | g = float32(0.05) 49 | jump = float32(-1.0) 50 | frames = 30 51 | interval = 120 52 | cloudStartX = 200 53 | clouds = []*cloud{} 54 | cloudX = 20 55 | holeYMax = 48 56 | cloudHeight = 8 57 | platforms = []*platform{{pY: 60}} 58 | scene = "title" 59 | score = 0 60 | isOnPlatform = false 61 | ) 62 | 63 | func draw() { 64 | switch scene { 65 | case "title": 66 | drawTitle() 67 | case "game": 68 | drawGame() 69 | case "gameover": 70 | drawGameover() 71 | } 72 | } 73 | 74 | func drawTitle() { 75 | koebiten.Println("click to start") 76 | if isAnyKeyJustPressed() { 77 | scene = "game" 78 | } 79 | } 80 | 81 | func drawGame() { 82 | koebiten.DrawImageFS(nil, fsys, "sky.png", 0, 0) 83 | koebiten.Println("Score", score) 84 | koebiten.DrawImageFS(nil, fsys, "platform.png", 0, 60) 85 | 86 | if y <= -10.0 { 87 | scene = "gameover" 88 | } 89 | 90 | for i, cloud := range clouds { 91 | if cloud.cloudX < int(x) { 92 | score = i + 1 93 | } 94 | } 95 | 96 | if isAnyKeyJustPressed() { 97 | vy = jump 98 | isOnPlatform = false 99 | } 100 | 101 | vy += g // 速度に加速度を足す 102 | y += vy // 位置に速度を足す 103 | 104 | for _, platform := range platforms { 105 | if hitPlatformRect(int(y), int(x), int(y)+22, int(x)+22, platform.pY, 0, platform.pY+22, 128) { 106 | isOnPlatform = true 107 | } 108 | } 109 | 110 | if isOnPlatform { 111 | vy = 0 112 | y = 33.5 113 | } 114 | 115 | frames++ 116 | walkGopher(x, y, frames) 117 | 118 | if frames%interval == 0 { 119 | cloud := &cloud{cloudStartX, rand.N(holeYMax)} 120 | clouds = append(clouds, cloud) 121 | } 122 | 123 | for _, cloud := range clouds { 124 | cloud.cloudX -= 1 // 少しずつ左へ 125 | drawWalls(cloud) 126 | 127 | if hitRects(int(x), int(y), int(x)+20, int(y)+8, cloud.cloudX, cloud.holeY-cloudHeight, cloud.cloudX+cloudX, cloud.holeY+cloudHeight) { 128 | scene = "gameover" 129 | } 130 | } 131 | } 132 | 133 | func drawGameover() { 134 | koebiten.Println("Game Over") 135 | koebiten.Println("Score:", score) 136 | 137 | if isAnyKeyJustPressed() { 138 | scene = "title" 139 | x, y, vy, score = 50.0, 30.0, 0.0, 0 140 | clouds = []*cloud{} 141 | } 142 | } 143 | 144 | func drawWalls(c *cloud) { 145 | koebiten.DrawImageFS(nil, fsys, "cloud.png", c.cloudX, c.holeY-cloudHeight) 146 | } 147 | 148 | func walkGopher(x, y float32, frames int) { 149 | img := "gopher.png" 150 | if frames%2 != 0 { 151 | img = "gopher_r.png" 152 | } 153 | koebiten.DrawImageFS(nil, fsys, img, int(x), int(y)) 154 | } 155 | 156 | func hitRects(aLeft, aTop, aRight, aBottom, bLeft, bTop, bRight, bBottom int) bool { 157 | return aTop < bBottom && bTop < aBottom && aLeft < bRight && bLeft < aRight 158 | } 159 | 160 | func hitPlatformRect(aTop, aLeft, aBottom, aRight, bTop, bLeft, bBottom, bRight int) bool { 161 | return aTop < bBottom && bTop < aBottom && aLeft < bRight && bLeft < aRight 162 | } 163 | 164 | func isAnyKeyJustPressed() bool { 165 | return len(koebiten.AppendJustPressedKeys(nil)) > 0 166 | } 167 | -------------------------------------------------------------------------------- /image.go: -------------------------------------------------------------------------------- 1 | package koebiten 2 | 3 | import ( 4 | "image/color" 5 | "io/fs" 6 | 7 | "github.com/chewxy/math32" 8 | "tinygo.org/x/drivers/image/png" 9 | "tinygo.org/x/drivers/pixel" 10 | ) 11 | 12 | var _ Displayer = (*Image)(nil) 13 | 14 | // Image is a Wrapper for the pixel.Image type. 15 | // 16 | // Image implements the Displayer interface. 17 | type Image struct { 18 | img pixel.Image[pixel.Monochrome] 19 | } 20 | 21 | // Size returns the width and height of the image. 22 | // 23 | // It implements the Displayer interface. 24 | func (i *Image) Size() (int16, int16) { 25 | x, y := i.img.Size() 26 | return int16(x), int16(y) 27 | } 28 | 29 | // SetPixel sets the pixel at the given x and y coordinates to the given color. 30 | // The color is converted to a pixel.Monochrome color. 31 | // If the x and y coordinates are outside the image, the function does nothing. 32 | // 33 | // It implements the Displayer interface. 34 | func (i *Image) SetPixel(x, y int16, c color.RGBA) { 35 | w, h := i.img.Size() 36 | if x < 0 || x >= int16(w) || y < 0 || y >= int16(h) { 37 | return 38 | } 39 | i.img.Set(int(x), int(y), pixel.NewMonochrome(c.R, c.G, c.B)) 40 | } 41 | 42 | // Display does nothing. 43 | // 44 | // It implements the Displayer interface. 45 | func (i *Image) Display() error { return nil } 46 | 47 | // ClearDisplay clears the display. 48 | // 49 | // It implements the Displayer interface. 50 | func (i *Image) ClearDisplay() {} 51 | 52 | // ClearBuffer clears the buffer. 53 | // 54 | // It implements the Displayer interface. 55 | func (i *Image) ClearBuffer() {} 56 | 57 | // NewImage creates a new Image with the given width and height. 58 | // 59 | // It returns a pointer to the new Image. 60 | func NewImage(width, height int16) *Image { 61 | return &Image{ 62 | img: pixel.NewImage[pixel.Monochrome](int(width), int(height)), 63 | } 64 | } 65 | 66 | // NewImageFromFS creates a new Image from the filesystem. 67 | func NewImageFromFS(fsys fs.FS, path string) *Image { 68 | img, err := loadImageFromFS(fsys, path) 69 | if err != nil { 70 | panic(err) 71 | } 72 | return &Image{img: img} 73 | } 74 | 75 | // loadImageFromFS loads an image from the filesystem. 76 | func loadImageFromFS(fsys fs.FS, path string) (pixel.Image[pixel.Monochrome], error) { 77 | var buffer [3 * 8 * 8 * 4]uint16 78 | p, err := fsys.Open(path) 79 | if err != nil { 80 | return pixel.Image[pixel.Monochrome]{}, err 81 | } 82 | 83 | var img pixel.Image[pixel.Monochrome] 84 | png.SetCallback(buffer[:], func(data []uint16, x, y, w, h, width, height int16) { 85 | if img.Len() == 0 { 86 | img = pixel.NewImage[pixel.Monochrome](int(width), int(height)) 87 | } 88 | 89 | for yy := int16(0); yy < h; yy++ { 90 | for xx := int16(0); xx < w; xx++ { 91 | c := C565toRGBA(data[yy*w+xx]) 92 | cnt := 0 93 | if c.R < 0x80 { 94 | cnt++ 95 | } 96 | if c.G < 0x80 { 97 | cnt++ 98 | } 99 | if c.B < 0x80 { 100 | cnt++ 101 | } 102 | if cnt >= 2 { 103 | img.Set(int(x+xx), int(y+yy), true) 104 | } 105 | } 106 | } 107 | }) 108 | 109 | if _, err = png.Decode(p); err != nil { 110 | return pixel.Image[pixel.Monochrome]{}, err 111 | } 112 | 113 | return img, nil 114 | } 115 | 116 | // Fill fills the image with the given color. 117 | func (i *Image) Fill(clr color.Color) { 118 | r, g, b, _ := clr.RGBA() 119 | w, h := i.img.Size() 120 | for y := 0; y < h; y++ { 121 | for x := 0; x < w; x++ { 122 | i.img.Set(x, y, pixel.NewMonochrome(uint8(r), uint8(g), uint8(b))) 123 | } 124 | } 125 | } 126 | 127 | type DrawImageOptions struct { 128 | GeoM GeoM 129 | } 130 | 131 | // DrawImage draws an image onto the display. 132 | func (i *Image) DrawImage(dst Displayer, options DrawImageOptions) { 133 | if isNil(dst) { 134 | dst = display 135 | } 136 | 137 | geoM := options.GeoM 138 | if !geoM.IsInvertible() { 139 | return 140 | } 141 | 142 | w, h := i.img.Size() 143 | if geoM.a_1 == 0 && geoM.b == 0 && geoM.c == 0 && geoM.d_1 == 0 { 144 | tx, ty := geoM.Apply(0, 0) 145 | ox, oy := int(math32.Round(tx)), int(math32.Round(ty)) 146 | for yy := 0; yy < h; yy++ { 147 | for xx := 0; xx < w; xx++ { 148 | if i.img.Get(xx, yy) == true { 149 | dst.SetPixel(int16(xx+ox), int16(yy+oy), white) 150 | } 151 | } 152 | } 153 | } else { 154 | for yy := 0; yy < h; yy++ { 155 | for xx := 0; xx < w; xx++ { 156 | if i.img.Get(xx, yy) == true { 157 | xxf, yyf := geoM.Apply(float32(xx), float32(yy)) 158 | dst.SetPixel(int16(math32.Round(xxf)), int16(math32.Round(yyf)), white) 159 | } 160 | } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /games/snakegame/snakegame/snakegame.go: -------------------------------------------------------------------------------- 1 | package snakegame 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/sago35/koebiten" 8 | "tinygo.org/x/drivers/pixel" 9 | ) 10 | 11 | var ( 12 | white = pixel.NewMonochrome(0xFF, 0xFF, 0xFF) 13 | black = pixel.NewMonochrome(0x00, 0x00, 0x00) 14 | 15 | gridSize = 4 16 | width = 128 / gridSize 17 | height = 64 / gridSize 18 | initialSpeed = 100 * time.Millisecond 19 | ) 20 | 21 | type Point struct { 22 | x, y int 23 | } 24 | 25 | type GameState int 26 | 27 | const ( 28 | StateOpening GameState = iota 29 | StatePlaying 30 | StateGameOver 31 | ) 32 | 33 | type Game struct { 34 | snake []Point 35 | snakeBuf [128 / 4 * 64 / 4]Point 36 | dir Point 37 | food Point 38 | alive bool 39 | speed time.Duration 40 | lastMove time.Time 41 | pendingDir Point 42 | score int 43 | state GameState 44 | waitCnt int 45 | } 46 | 47 | func NewGame() *Game { 48 | game := &Game{} 49 | game.Init() 50 | return game 51 | } 52 | 53 | func (g *Game) Init() { 54 | g.snake = g.snakeBuf[:1] 55 | g.snake[0] = Point{width / 2, height / 2} 56 | g.dir = Point{1, 0} 57 | g.pendingDir = g.dir 58 | g.spawnFood() 59 | g.alive = true 60 | g.speed = initialSpeed 61 | g.lastMove = time.Now() 62 | g.score = 0 63 | } 64 | 65 | func (g *Game) spawnFood() { 66 | g.food = Point{rand.Intn(width), rand.Intn(height)} 67 | } 68 | 69 | func (g *Game) Update() error { 70 | if g.state == StateOpening { 71 | if isAnyKeyJustPressed() { 72 | g.Init() 73 | g.state = StatePlaying 74 | g.waitCnt = 32 75 | } 76 | return nil 77 | } 78 | if g.state == StateGameOver { 79 | if g.waitCnt == 0 { 80 | if isAnyKeyJustPressed() { 81 | g.Init() 82 | g.state = StatePlaying 83 | g.waitCnt = 32 84 | } 85 | } else if g.waitCnt > 0 { 86 | g.waitCnt-- 87 | } 88 | return nil 89 | } 90 | 91 | if !g.alive { 92 | g.state = StateGameOver 93 | return nil 94 | } 95 | 96 | if koebiten.IsKeyPressed(koebiten.KeyArrowUp) && g.dir.y == 0 { 97 | g.pendingDir = Point{0, -1} 98 | } 99 | if koebiten.IsKeyPressed(koebiten.KeyArrowDown) && g.dir.y == 0 { 100 | g.pendingDir = Point{0, 1} 101 | } 102 | if koebiten.IsKeyPressed(koebiten.KeyArrowRight) && g.dir.x == 0 { 103 | g.pendingDir = Point{1, 0} 104 | } 105 | if koebiten.IsKeyPressed(koebiten.KeyArrowLeft) && g.dir.x == 0 { 106 | g.pendingDir = Point{-1, 0} 107 | } 108 | if koebiten.IsKeyPressed(koebiten.KeyRotaryRight) { 109 | if g.dir.x == 1 { 110 | g.pendingDir = Point{0, 1} 111 | } else if g.dir.y == 1 { 112 | g.pendingDir = Point{-1, 0} 113 | } else if g.dir.x == -1 { 114 | g.pendingDir = Point{0, -1} 115 | } else if g.dir.y == -1 { 116 | g.pendingDir = Point{1, 0} 117 | } 118 | } 119 | if koebiten.IsKeyPressed(koebiten.KeyRotaryLeft) { 120 | if g.dir.x == 1 { 121 | g.pendingDir = Point{0, -1} 122 | } else if g.dir.y == 1 { 123 | g.pendingDir = Point{1, 0} 124 | } else if g.dir.x == -1 { 125 | g.pendingDir = Point{0, 1} 126 | } else if g.dir.y == -1 { 127 | g.pendingDir = Point{-1, 0} 128 | } 129 | } 130 | 131 | if time.Since(g.lastMove) < g.speed { 132 | return nil 133 | } 134 | g.lastMove = time.Now() 135 | g.dir = g.pendingDir 136 | 137 | next := Point{(g.snake[0].x + g.dir.x + width) % width, (g.snake[0].y + g.dir.y + height) % height} 138 | for _, s := range g.snake { 139 | if s == next { 140 | g.alive = false 141 | g.state = StateGameOver 142 | return nil 143 | } 144 | } 145 | 146 | g.snake = g.snakeBuf[:len(g.snake)+1] 147 | for i := len(g.snake) - 1; i > 0; i-- { 148 | g.snake[i] = g.snake[i-1] 149 | } 150 | g.snake[0] = next 151 | if next == g.food { 152 | g.spawnFood() 153 | g.score = len(g.snake) - 1 154 | g.speed = time.Duration(float32(g.speed) * 0.95) 155 | } else { 156 | g.snake = g.snake[:len(g.snake)-1] 157 | } 158 | 159 | return nil 160 | } 161 | 162 | func (g *Game) Draw(screen *koebiten.Image) { 163 | if g.state == StateOpening { 164 | koebiten.Println("Press Button to Start") 165 | return 166 | } 167 | if g.state == StateGameOver { 168 | koebiten.Println("Game Over") 169 | koebiten.Println("Score:", g.score) 170 | if g.waitCnt == 0 { 171 | koebiten.Println("Press Button to Restart") 172 | } 173 | return 174 | } 175 | 176 | for _, s := range g.snake { 177 | koebiten.DrawFilledRect(screen, s.x*gridSize, s.y*gridSize, gridSize, gridSize, white) 178 | } 179 | koebiten.DrawFilledRect(screen, g.food.x*gridSize, g.food.y*gridSize, gridSize, gridSize, white) 180 | koebiten.Println("Score:", g.score) 181 | } 182 | 183 | func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { 184 | return 128, 64 185 | } 186 | 187 | func isAnyKeyJustPressed() bool { 188 | return len(koebiten.AppendJustPressedKeys(nil)) > 0 189 | } 190 | -------------------------------------------------------------------------------- /hardware/pybadge.go: -------------------------------------------------------------------------------- 1 | //go:build tinygo && pybadge 2 | 3 | package hardware 4 | 5 | import ( 6 | "image/color" 7 | "machine" 8 | 9 | "github.com/sago35/koebiten" 10 | "tinygo.org/x/drivers/pixel" 11 | "tinygo.org/x/drivers/shifter" 12 | "tinygo.org/x/drivers/st7735" 13 | "tinygo.org/x/tinydraw" 14 | ) 15 | 16 | var Device = &device{} 17 | 18 | type device struct { 19 | display *Display 20 | gpioPins []uint8 21 | buttons shifter.Device 22 | state []State 23 | cycle []int 24 | keybuf [1]koebiten.Key 25 | } 26 | 27 | const ( 28 | debounce = 0 29 | ) 30 | 31 | type State uint8 32 | 33 | const ( 34 | None State = iota 35 | NoneToPress 36 | Press 37 | PressToRelease 38 | ) 39 | 40 | func (z *device) Init() error { 41 | machine.SPI1.Configure(machine.SPIConfig{ 42 | SCK: machine.SPI1_SCK_PIN, 43 | SDO: machine.SPI1_SDO_PIN, 44 | SDI: machine.SPI1_SDI_PIN, 45 | Frequency: 8 * machine.MHz, 46 | }) 47 | 48 | d := st7735.New(machine.SPI1, machine.TFT_RST, machine.TFT_DC, machine.TFT_CS, machine.TFT_LITE) 49 | d.Configure(st7735.Config{ 50 | Rotation: st7735.ROTATION_90, 51 | }) 52 | 53 | d.FillScreen(color.RGBA{0, 0, 0, 255}) 54 | z.display = InitDisplay(&d, 128, 64) 55 | 56 | z.buttons = shifter.NewButtons() 57 | z.buttons.Configure() 58 | 59 | z.gpioPins = []uint8{ 60 | shifter.BUTTON_A, 61 | shifter.BUTTON_B, 62 | shifter.BUTTON_SELECT, 63 | shifter.BUTTON_START, 64 | 0xFF, 65 | 0xFF, 66 | 0xFF, 67 | 0xFF, 68 | 0xFF, 69 | 0xFF, 70 | 0xFF, 71 | 0xFF, 72 | 0xFF, 73 | 0xFF, 74 | 0xFF, 75 | 0xFF, 76 | shifter.BUTTON_LEFT, 77 | shifter.BUTTON_RIGHT, 78 | shifter.BUTTON_UP, 79 | shifter.BUTTON_DOWN, 80 | } 81 | 82 | z.state = make([]State, len(z.gpioPins)) 83 | z.cycle = make([]int, len(z.gpioPins)) 84 | return nil 85 | } 86 | 87 | func (z *device) GetDisplay() koebiten.Displayer { 88 | return z.display 89 | } 90 | 91 | func (z *device) KeyUpdate() error { 92 | buf := z.keybuf[:] 93 | z.buttons.ReadInput() 94 | for i, r := range z.gpioPins { 95 | current := false 96 | if r != 0xFF { 97 | current = z.buttons.Pins[r].Get() 98 | } 99 | idx := i 100 | 101 | switch z.state[idx] { 102 | case None: 103 | if current { 104 | if z.cycle[idx] >= debounce { 105 | z.state[idx] = NoneToPress 106 | z.cycle[idx] = 0 107 | } else { 108 | z.cycle[idx]++ 109 | } 110 | } else { 111 | z.cycle[idx] = 0 112 | } 113 | case NoneToPress: 114 | z.state[idx] = Press 115 | buf[0] = koebiten.Key(idx) 116 | koebiten.AppendJustPressedKeys(buf) 117 | case Press: 118 | buf[0] = koebiten.Key(idx) 119 | koebiten.AppendPressedKeys(buf) 120 | if current { 121 | z.cycle[idx] = 0 122 | } else { 123 | if z.cycle[idx] >= debounce { 124 | z.state[idx] = PressToRelease 125 | z.cycle[idx] = 0 126 | } else { 127 | z.cycle[idx]++ 128 | } 129 | } 130 | case PressToRelease: 131 | z.state[idx] = None 132 | buf[0] = koebiten.Key(idx) 133 | koebiten.AppendJustReleasedKeys(buf) 134 | } 135 | } 136 | return nil 137 | } 138 | 139 | type Display struct { 140 | d *st7735.Device 141 | img pixel.Image[pixel.RGB565BE] 142 | } 143 | 144 | func InitDisplay(dev *st7735.Device, width, height int) *Display { 145 | d := &Display{ 146 | d: dev, 147 | img: pixel.NewImage[pixel.RGB565BE](width, height), 148 | } 149 | 150 | ox, oy := d.getImageTopLeftForCentering() 151 | w, h := d.img.Size() 152 | tinydraw.Rectangle(dev, ox-1, oy-1, int16(w)+2, int16(h)+2, white) 153 | 154 | return d 155 | } 156 | 157 | func (d *Display) Size() (x, y int16) { 158 | return 128, 64 159 | } 160 | 161 | func (d *Display) SetPixel(x, y int16, c color.RGBA) { 162 | mx, my := d.Size() 163 | if 0 <= x && x < int16(mx) && 0 <= y && y < int16(my) { 164 | d.img.Set(int(x), int(y), pixelWhite) 165 | } 166 | return 167 | cnt := 0 168 | if c.R < 0x80 { 169 | cnt++ 170 | } 171 | if c.G < 0x80 { 172 | cnt++ 173 | } 174 | if c.B < 0x80 { 175 | cnt++ 176 | } 177 | if cnt >= 2 { 178 | d.img.Set(int(x), int(y), pixelWhite) 179 | } else { 180 | d.img.Set(int(x), int(y), pixelBlack) 181 | } 182 | //d.d.SetPixel(x, y, c) 183 | } 184 | 185 | func (d *Display) Display() error { 186 | ox, oy := d.getImageTopLeftForCentering() 187 | return d.d.DrawBitmap(int16(ox), int16(oy), d.img) 188 | } 189 | 190 | func (d *Display) ClearBuffer() { 191 | d.img.FillSolidColor(pixelBlack) 192 | } 193 | 194 | func (d *Display) ClearDisplay() { 195 | } 196 | 197 | func (d *Display) getImageTopLeftForCentering() (int16, int16) { 198 | mx, my := d.img.Size() 199 | ox := (160 - mx) / 2 200 | oy := (128 - my) / 2 201 | return int16(ox), int16(oy) 202 | } 203 | 204 | var ( 205 | white = color.RGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF} 206 | black = color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF} 207 | 208 | pixelWhite = pixel.NewColor[pixel.RGB565BE](0xFF, 0xFF, 0xFF) 209 | pixelBlack = pixel.NewColor[pixel.RGB565BE](0x00, 0x00, 0x00) 210 | ) 211 | -------------------------------------------------------------------------------- /inpututil.go: -------------------------------------------------------------------------------- 1 | package koebiten 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type Key int 8 | 9 | const ( 10 | KeyMax = 20 11 | ) 12 | 13 | const ( 14 | Key0 Key = iota 15 | Key1 16 | Key2 17 | Key3 18 | Key4 19 | Key5 20 | Key6 21 | Key7 22 | Key8 23 | Key9 24 | Key10 25 | Key11 26 | KeyRotaryButton 27 | KeyJoystick 28 | KeyRotaryLeft 29 | KeyRotaryRight 30 | KeyLeft 31 | KeyRight 32 | KeyUp 33 | KeyDown 34 | ) 35 | 36 | const ( 37 | KeyArrowLeft = KeyLeft 38 | KeyArrowRight = KeyRight 39 | KeyArrowUp = KeyUp 40 | KeyArrowDown = KeyDown 41 | ) 42 | 43 | type pos struct { 44 | x int 45 | y int 46 | } 47 | 48 | type inputState struct { 49 | keyDurations []int 50 | prevKeyDurations []int 51 | state []bool 52 | 53 | m sync.RWMutex 54 | } 55 | 56 | var theInputState = &inputState{ 57 | keyDurations: make([]int, KeyMax+1), 58 | prevKeyDurations: make([]int, KeyMax+1), 59 | state: make([]bool, KeyMax+1), 60 | } 61 | 62 | var isKeyPressed = make([]bool, KeyMax) 63 | 64 | func (i *inputState) update() { 65 | i.m.Lock() 66 | defer i.m.Unlock() 67 | 68 | // Keyboard 69 | copy(i.prevKeyDurations, i.keyDurations) 70 | for k := Key(0); k <= KeyMax; k++ { 71 | if i.state[k] { 72 | i.keyDurations[k]++ 73 | } else { 74 | i.keyDurations[k] = 0 75 | } 76 | } 77 | } 78 | 79 | // AppendPressedKeys append currently pressed keyboard keys to keys and returns the extended buffer. 80 | // Giving a slice that already has enough capacity works efficiently. 81 | // 82 | // AppendPressedKeys must be called in a game's Update, not Draw. 83 | // 84 | // AppendPressedKeys is concurrent safe. 85 | func AppendPressedKeys(keys []Key) []Key { 86 | theInputState.m.Lock() 87 | defer theInputState.m.Unlock() 88 | 89 | for _, k := range keys { 90 | theInputState.state[k] = true 91 | } 92 | 93 | for i, d := range theInputState.keyDurations { 94 | if d == 0 { 95 | continue 96 | } 97 | keys = append(keys, Key(i)) 98 | } 99 | return keys 100 | } 101 | 102 | // PressedKeys returns a set of currently pressed keyboard keys. 103 | // 104 | // PressedKeys must be called in a game's Update, not Draw. 105 | // 106 | // Deprecated: as of v2.2. Use AppendPressedKeys instead. 107 | func PressedKeys() []Key { 108 | return AppendPressedKeys(nil) 109 | } 110 | 111 | // AppendJustPressedKeys append just pressed keyboard keys to keys and returns the extended buffer. 112 | // Giving a slice that already has enough capacity works efficiently. 113 | // 114 | // AppendJustPressedKeys must be called in a game's Update, not Draw. 115 | // 116 | // AppendJustPressedKeys is concurrent safe. 117 | func AppendJustPressedKeys(keys []Key) []Key { 118 | theInputState.m.Lock() 119 | defer theInputState.m.Unlock() 120 | 121 | for _, k := range keys { 122 | theInputState.state[k] = true 123 | } 124 | 125 | for i, d := range theInputState.keyDurations { 126 | if d != 1 { 127 | continue 128 | } 129 | keys = append(keys, Key(i)) 130 | } 131 | return keys 132 | } 133 | 134 | // AppendJustReleasedKeys append just released keyboard keys to keys and returns the extended buffer. 135 | // Giving a slice that already has enough capacity works efficiently. 136 | // 137 | // AppendJustReleasedKeys must be called in a game's Update, not Draw. 138 | // 139 | // AppendJustReleasedKeys is concurrent safe. 140 | func AppendJustReleasedKeys(keys []Key) []Key { 141 | theInputState.m.Lock() 142 | defer theInputState.m.Unlock() 143 | 144 | for _, k := range keys { 145 | theInputState.state[k] = false 146 | } 147 | 148 | for k := Key(0); k <= KeyMax; k++ { 149 | if theInputState.keyDurations[k] != 0 { 150 | continue 151 | } 152 | if theInputState.prevKeyDurations[k] == 0 { 153 | continue 154 | } 155 | keys = append(keys, k) 156 | } 157 | return keys 158 | } 159 | 160 | // IsKeyJustPressed returns a boolean value indicating 161 | // whether the given key is pressed just in the current tick. 162 | // 163 | // IsKeyJustPressed must be called in a game's Update, not Draw. 164 | // 165 | // IsKeyJustPressed is concurrent safe. 166 | func IsKeyJustPressed(key Key) bool { 167 | return KeyPressDuration(key) == 1 168 | } 169 | 170 | func IsKeyPressed(key Key) bool { 171 | return KeyPressDuration(key) > 0 172 | } 173 | 174 | // IsKeyJustReleased returns a boolean value indicating 175 | // whether the given key is released just in the current tick. 176 | // 177 | // IsKeyJustReleased must be called in a game's Update, not Draw. 178 | // 179 | // IsKeyJustReleased is concurrent safe. 180 | func IsKeyJustReleased(key Key) bool { 181 | theInputState.m.RLock() 182 | r := theInputState.keyDurations[key] == 0 && theInputState.prevKeyDurations[key] > 0 183 | theInputState.m.RUnlock() 184 | return r 185 | } 186 | 187 | // KeyPressDuration returns how long the key is pressed in ticks (Update). 188 | // 189 | // KeyPressDuration must be called in a game's Update, not Draw. 190 | // 191 | // KeyPressDuration is concurrent safe. 192 | func KeyPressDuration(key Key) int { 193 | theInputState.m.RLock() 194 | s := theInputState.keyDurations[key] 195 | theInputState.m.RUnlock() 196 | return s 197 | } 198 | -------------------------------------------------------------------------------- /hardware/gopher-badge.go: -------------------------------------------------------------------------------- 1 | //go:build tinygo && gopher_badge 2 | 3 | package hardware 4 | 5 | import ( 6 | "image/color" 7 | "machine" 8 | 9 | "github.com/sago35/koebiten" 10 | "tinygo.org/x/drivers/pixel" 11 | "tinygo.org/x/drivers/st7789" 12 | "tinygo.org/x/tinydraw" 13 | ) 14 | 15 | var Device = &device{} 16 | 17 | type device struct { 18 | display *Display 19 | gpioPins []machine.Pin 20 | state []State 21 | cycle []int 22 | keybuf [1]koebiten.Key 23 | } 24 | 25 | const ( 26 | debounce = 0 27 | ) 28 | 29 | type State uint8 30 | 31 | const ( 32 | None State = iota 33 | NoneToPress 34 | Press 35 | PressToRelease 36 | ) 37 | 38 | func (z *device) Init() error { 39 | machine.SPI0.Configure(machine.SPIConfig{ 40 | Frequency: 48 * machine.MHz, 41 | Mode: 0, 42 | }) 43 | 44 | d := st7789.New(machine.SPI0, 45 | machine.TFT_RST, // TFT_RESET 46 | machine.TFT_WRX, // TFT_DC 47 | machine.TFT_CS, // TFT_CS 48 | machine.TFT_BACKLIGHT) // TFT_LITE 49 | 50 | d.Configure(st7789.Config{ 51 | Rotation: st7789.ROTATION_270, 52 | Height: 320, 53 | }) 54 | //d.ClearDisplay() 55 | z.display = InitDisplay(&d, 128, 64) 56 | 57 | gpioPins := []machine.Pin{ 58 | machine.BUTTON_A, 59 | machine.BUTTON_B, 60 | machine.BUTTON_UP, 61 | machine.BUTTON_LEFT, 62 | machine.BUTTON_DOWN, 63 | machine.BUTTON_RIGHT, 64 | } 65 | 66 | for i := range gpioPins { 67 | gpioPins[i].Configure(machine.PinConfig{Mode: machine.PinInputPullup}) 68 | } 69 | 70 | z.gpioPins = []machine.Pin{ 71 | machine.BUTTON_A, 72 | machine.BUTTON_B, 73 | machine.NoPin, 74 | machine.NoPin, 75 | machine.NoPin, 76 | machine.NoPin, 77 | machine.NoPin, 78 | machine.NoPin, 79 | machine.NoPin, 80 | machine.NoPin, 81 | machine.NoPin, 82 | machine.NoPin, 83 | machine.NoPin, 84 | machine.NoPin, 85 | machine.NoPin, 86 | machine.NoPin, 87 | machine.BUTTON_LEFT, 88 | machine.BUTTON_RIGHT, 89 | machine.BUTTON_UP, 90 | machine.BUTTON_DOWN, 91 | } 92 | 93 | z.state = make([]State, len(z.gpioPins)) 94 | z.cycle = make([]int, len(z.gpioPins)) 95 | return nil 96 | } 97 | 98 | func (z *device) GetDisplay() koebiten.Displayer { 99 | return z.display 100 | } 101 | 102 | func (z *device) KeyUpdate() error { 103 | buf := z.keybuf[:] 104 | for r := range z.gpioPins { 105 | current := !z.gpioPins[r].Get() 106 | if z.gpioPins[r] == machine.NoPin { 107 | current = false 108 | } 109 | idx := r 110 | 111 | switch z.state[idx] { 112 | case None: 113 | if current { 114 | if z.cycle[idx] >= debounce { 115 | z.state[idx] = NoneToPress 116 | z.cycle[idx] = 0 117 | } else { 118 | z.cycle[idx]++ 119 | } 120 | } else { 121 | z.cycle[idx] = 0 122 | } 123 | case NoneToPress: 124 | z.state[idx] = Press 125 | buf[0] = koebiten.Key(idx) 126 | koebiten.AppendJustPressedKeys(buf) 127 | case Press: 128 | buf[0] = koebiten.Key(idx) 129 | koebiten.AppendPressedKeys(buf) 130 | if current { 131 | z.cycle[idx] = 0 132 | } else { 133 | if z.cycle[idx] >= debounce { 134 | z.state[idx] = PressToRelease 135 | z.cycle[idx] = 0 136 | } else { 137 | z.cycle[idx]++ 138 | } 139 | } 140 | case PressToRelease: 141 | z.state[idx] = None 142 | buf[0] = koebiten.Key(idx) 143 | koebiten.AppendJustReleasedKeys(buf) 144 | } 145 | } 146 | return nil 147 | } 148 | 149 | type Display struct { 150 | d *st7789.Device 151 | img pixel.Image[pixel.RGB565BE] 152 | } 153 | 154 | func InitDisplay(dev *st7789.Device, width, height int) *Display { 155 | d := &Display{ 156 | d: dev, 157 | img: pixel.NewImage[pixel.RGB565BE](width*2+1, height*2+1), 158 | } 159 | 160 | ox, oy := d.getImageTopLeftForCentering() 161 | w, h := d.img.Size() 162 | tinydraw.Rectangle(dev, ox-1, oy-1, int16(w)+2, int16(h)+2, white) 163 | 164 | return d 165 | } 166 | 167 | func (d *Display) Size() (x, y int16) { 168 | return 128, 64 169 | } 170 | 171 | func (d *Display) SetPixel(x, y int16, c color.RGBA) { 172 | mx, my := d.Size() 173 | if 0 <= x && x < int16(mx) && 0 <= y && y < int16(my) { 174 | d.img.Set(int(x*2+0), int(y*2+0), pixelWhite) 175 | d.img.Set(int(x*2+1), int(y*2+0), pixelWhite) 176 | d.img.Set(int(x*2+0), int(y*2+1), pixelWhite) 177 | d.img.Set(int(x*2+1), int(y*2+1), pixelWhite) 178 | } 179 | return 180 | cnt := 0 181 | if c.R < 0x80 { 182 | cnt++ 183 | } 184 | if c.G < 0x80 { 185 | cnt++ 186 | } 187 | if c.B < 0x80 { 188 | cnt++ 189 | } 190 | if cnt >= 2 { 191 | d.img.Set(int(x), int(y), pixelWhite) 192 | } else { 193 | d.img.Set(int(x), int(y), pixelBlack) 194 | } 195 | //d.d.SetPixel(x, y, c) 196 | } 197 | 198 | func (d *Display) Display() error { 199 | ox, oy := d.getImageTopLeftForCentering() 200 | return d.d.DrawBitmap(int16(ox), int16(oy), d.img) 201 | } 202 | 203 | func (d *Display) ClearBuffer() { 204 | d.img.FillSolidColor(pixelBlack) 205 | } 206 | 207 | func (d *Display) ClearDisplay() { 208 | } 209 | 210 | func (d *Display) getImageTopLeftForCentering() (int16, int16) { 211 | mx, my := d.img.Size() 212 | ox := (320 - mx) / 2 213 | oy := (240 - my) / 2 214 | return int16(ox), int16(oy) 215 | } 216 | 217 | var ( 218 | white = color.RGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF} 219 | black = color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF} 220 | 221 | pixelWhite = pixel.NewColor[pixel.RGB565BE](0xFF, 0xFF, 0xFF) 222 | pixelBlack = pixel.NewColor[pixel.RGB565BE](0x00, 0x00, 0x00) 223 | ) 224 | -------------------------------------------------------------------------------- /hardware/gopher-board-spi.go: -------------------------------------------------------------------------------- 1 | //go:build tinygo && gopher_board_spi 2 | 3 | package hardware 4 | 5 | import ( 6 | "image/color" 7 | "machine" 8 | 9 | "github.com/sago35/koebiten" 10 | "tinygo.org/x/drivers/pixel" 11 | "tinygo.org/x/drivers/st7789" 12 | "tinygo.org/x/tinydraw" 13 | ) 14 | 15 | var ( 16 | Device = &device{} 17 | gpioPins []machine.Pin 18 | ) 19 | 20 | type device struct { 21 | display *Display 22 | gpioPins []machine.Pin 23 | state []State 24 | cycle []int 25 | keybuf [1]koebiten.Key 26 | } 27 | 28 | const ( 29 | debounce = 0 30 | ) 31 | 32 | type State uint8 33 | 34 | const ( 35 | None State = iota 36 | NoneToPress 37 | Press 38 | PressToRelease 39 | ) 40 | 41 | func (z *device) GetDisplay() koebiten.Displayer { 42 | return z.display 43 | } 44 | 45 | func (z *device) Init() error { 46 | machine.SPI1.Configure(machine.SPIConfig{ 47 | Frequency: 16000000, 48 | Mode: 0, 49 | }) 50 | 51 | d := st7789.New(machine.SPI1, 52 | machine.GPIO9, // TFT_RESET 53 | machine.GPIO12, // TFT_DC 54 | machine.GPIO13, // TFT_CS 55 | machine.GPIO14) // TFT_LITE 56 | 57 | d.Configure(st7789.Config{ 58 | Rotation: st7789.ROTATION_90, 59 | Height: 240, 60 | Width: 240, 61 | }) 62 | d.FillRectangle(0, 0, 240, 240, black) 63 | z.display = InitDisplay(&d, 100, 64) 64 | 65 | gpioPins = []machine.Pin{ 66 | machine.GPIO3, // up 67 | machine.GPIO4, // left 68 | machine.GPIO6, // down 69 | machine.GPIO5, // right 70 | machine.GPIO26, // A 71 | machine.GPIO15, // B 72 | } 73 | 74 | for _, p := range gpioPins { 75 | p.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) 76 | } 77 | 78 | z.gpioPins = []machine.Pin{ 79 | machine.GPIO26, // A 80 | machine.GPIO15, // B 81 | machine.NoPin, 82 | machine.NoPin, 83 | machine.NoPin, 84 | machine.NoPin, 85 | machine.NoPin, 86 | machine.NoPin, 87 | machine.NoPin, 88 | machine.NoPin, 89 | machine.NoPin, 90 | machine.NoPin, 91 | machine.NoPin, 92 | machine.NoPin, 93 | machine.NoPin, 94 | machine.NoPin, 95 | machine.GPIO4, // left 96 | machine.GPIO5, // right 97 | machine.GPIO3, // up 98 | machine.GPIO6, // down 99 | } 100 | 101 | z.state = make([]State, len(z.gpioPins)) 102 | z.cycle = make([]int, len(z.gpioPins)) 103 | return nil 104 | } 105 | 106 | func (z *device) KeyUpdate() error { 107 | buf := z.keybuf[:] 108 | for r := range z.gpioPins { 109 | current := !z.gpioPins[r].Get() 110 | if z.gpioPins[r] == machine.NoPin { 111 | current = false 112 | } 113 | idx := r 114 | 115 | switch z.state[idx] { 116 | case None: 117 | if current { 118 | if z.cycle[idx] >= debounce { 119 | z.state[idx] = NoneToPress 120 | z.cycle[idx] = 0 121 | } else { 122 | z.cycle[idx]++ 123 | } 124 | } else { 125 | z.cycle[idx] = 0 126 | } 127 | case NoneToPress: 128 | z.state[idx] = Press 129 | buf[0] = koebiten.Key(idx) 130 | koebiten.AppendJustPressedKeys(buf) 131 | case Press: 132 | buf[0] = koebiten.Key(idx) 133 | koebiten.AppendPressedKeys(buf) 134 | if current { 135 | z.cycle[idx] = 0 136 | } else { 137 | if z.cycle[idx] >= debounce { 138 | z.state[idx] = PressToRelease 139 | z.cycle[idx] = 0 140 | } else { 141 | z.cycle[idx]++ 142 | } 143 | } 144 | case PressToRelease: 145 | z.state[idx] = None 146 | buf[0] = koebiten.Key(idx) 147 | koebiten.AppendJustReleasedKeys(buf) 148 | } 149 | } 150 | return nil 151 | } 152 | 153 | type Display struct { 154 | d *st7789.Device 155 | img pixel.Image[pixel.RGB565BE] 156 | } 157 | 158 | func InitDisplay(dev *st7789.Device, width, height int) *Display { 159 | d := &Display{ 160 | d: dev, 161 | img: pixel.NewImage[pixel.RGB565BE](width*2+1, height*2+1), 162 | } 163 | 164 | ox, oy := d.getImageTopLeftForCentering() 165 | w, h := d.img.Size() 166 | tinydraw.Rectangle(dev, ox-1, oy-1, int16(w)+2, int16(h)+2, white) 167 | 168 | return d 169 | } 170 | 171 | func (d *Display) Size() (x, y int16) { 172 | return 100, 64 173 | } 174 | 175 | func (d *Display) SetPixel(x, y int16, c color.RGBA) { 176 | mx, my := d.Size() 177 | if 0 <= x && x < int16(mx) && 0 <= y && y < int16(my) { 178 | d.img.Set(int(x*2+0), int(y*2+0), pixelWhite) 179 | d.img.Set(int(x*2+1), int(y*2+0), pixelWhite) 180 | d.img.Set(int(x*2+0), int(y*2+1), pixelWhite) 181 | d.img.Set(int(x*2+1), int(y*2+1), pixelWhite) 182 | } 183 | return 184 | cnt := 0 185 | if c.R < 0x80 { 186 | cnt++ 187 | } 188 | if c.G < 0x80 { 189 | cnt++ 190 | } 191 | if c.B < 0x80 { 192 | cnt++ 193 | } 194 | if cnt >= 2 { 195 | d.img.Set(int(x), int(y), pixelWhite) 196 | } else { 197 | d.img.Set(int(x), int(y), pixelBlack) 198 | } 199 | //d.d.SetPixel(x, y, c) 200 | } 201 | 202 | func (d *Display) Display() error { 203 | ox, oy := d.getImageTopLeftForCentering() 204 | return d.d.DrawBitmap(int16(ox), int16(oy), d.img) 205 | } 206 | 207 | func (d *Display) ClearBuffer() { 208 | d.img.FillSolidColor(pixelBlack) 209 | } 210 | 211 | func (d *Display) ClearDisplay() { 212 | } 213 | 214 | func (d *Display) getImageTopLeftForCentering() (int16, int16) { 215 | mx, my := d.img.Size() 216 | ox := (240 - mx) / 2 217 | oy := (240 - my) / 2 218 | return int16(ox), int16(oy) 219 | } 220 | 221 | var ( 222 | white = color.RGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF} 223 | black = color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF} 224 | 225 | pixelWhite = pixel.NewColor[pixel.RGB565BE](0xFF, 0xFF, 0xFF) 226 | pixelBlack = pixel.NewColor[pixel.RGB565BE](0x00, 0x00, 0x00) 227 | ) 228 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | koebiten wasm 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 107 | 108 | 109 | 110 |
111 |
112 |
113 |
114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |
up:ArrowUp e k
down:ArrowDown d j
left:ArrowLeft s h
right:ArrowRight f l
Key0:z n 0 Space Enter Tap
Key1:x m 1
Key2:c , 2
Key3:v . 3
124 |
125 |
126 | 127 | 128 | 129 | 130 | 131 |
132 |

The Go gopher was designed by Renée French.

133 | 134 | 135 | -------------------------------------------------------------------------------- /hardware/wioterminal.go: -------------------------------------------------------------------------------- 1 | //go:build tinygo && wioterminal 2 | 3 | package hardware 4 | 5 | import ( 6 | "image/color" 7 | "machine" 8 | 9 | "github.com/sago35/koebiten" 10 | "tinygo.org/x/drivers/ili9341" 11 | "tinygo.org/x/drivers/pixel" 12 | "tinygo.org/x/tinydraw" 13 | ) 14 | 15 | var Device = &device{} 16 | 17 | type device struct { 18 | display *Display 19 | gpioPins []machine.Pin 20 | state []State 21 | cycle []int 22 | keybuf [1]koebiten.Key 23 | } 24 | 25 | const ( 26 | debounce = 0 27 | ) 28 | 29 | type State uint8 30 | 31 | const ( 32 | None State = iota 33 | NoneToPress 34 | Press 35 | PressToRelease 36 | ) 37 | 38 | func (z *device) Init() error { 39 | machine.SPI3.Configure(machine.SPIConfig{ 40 | SCK: machine.LCD_SCK_PIN, 41 | SDO: machine.LCD_SDO_PIN, 42 | SDI: machine.LCD_SDI_PIN, 43 | Frequency: 40000000, 44 | }) 45 | 46 | // configure backlight 47 | backlight := machine.LCD_BACKLIGHT 48 | backlight.Configure(machine.PinConfig{machine.PinOutput}) 49 | 50 | d := ili9341.NewSPI( 51 | machine.SPI3, 52 | machine.LCD_DC, 53 | machine.LCD_SS_PIN, 54 | machine.LCD_RESET, 55 | ) 56 | 57 | // configure display 58 | d.Configure(ili9341.Config{}) 59 | d.SetRotation(ili9341.Rotation270) 60 | d.FillRectangle(0, 0, 320, 240, black) 61 | 62 | backlight.High() 63 | 64 | machine.SPI0.Configure(machine.SPIConfig{ 65 | Frequency: 32 * machine.MHz, 66 | Mode: 0, 67 | }) 68 | 69 | z.display = InitDisplay(d, 128, 64) 70 | 71 | gpioPins := []machine.Pin{ 72 | machine.WIO_KEY_A, 73 | machine.WIO_KEY_B, 74 | machine.WIO_KEY_C, 75 | machine.WIO_5S_PRESS, 76 | machine.WIO_5S_UP, 77 | machine.WIO_5S_LEFT, 78 | machine.WIO_5S_DOWN, 79 | machine.WIO_5S_RIGHT, 80 | } 81 | 82 | for i := range gpioPins { 83 | gpioPins[i].Configure(machine.PinConfig{Mode: machine.PinInputPullup}) 84 | } 85 | 86 | z.gpioPins = []machine.Pin{ 87 | machine.WIO_KEY_A, 88 | machine.WIO_KEY_B, 89 | machine.WIO_KEY_C, 90 | machine.WIO_5S_PRESS, 91 | machine.NoPin, 92 | machine.NoPin, 93 | machine.NoPin, 94 | machine.NoPin, 95 | machine.NoPin, 96 | machine.NoPin, 97 | machine.NoPin, 98 | machine.NoPin, 99 | machine.NoPin, 100 | machine.NoPin, 101 | machine.NoPin, 102 | machine.NoPin, 103 | machine.WIO_5S_LEFT, 104 | machine.WIO_5S_RIGHT, 105 | machine.WIO_5S_UP, 106 | machine.WIO_5S_DOWN, 107 | } 108 | 109 | z.state = make([]State, len(z.gpioPins)) 110 | z.cycle = make([]int, len(z.gpioPins)) 111 | return nil 112 | } 113 | 114 | func (z *device) GetDisplay() koebiten.Displayer { 115 | return z.display 116 | } 117 | 118 | func (z *device) KeyUpdate() error { 119 | buf := z.keybuf[:] 120 | for r := range z.gpioPins { 121 | current := false 122 | if z.gpioPins[r] != machine.NoPin { 123 | current = !z.gpioPins[r].Get() 124 | } 125 | idx := r 126 | 127 | switch z.state[idx] { 128 | case None: 129 | if current { 130 | if z.cycle[idx] >= debounce { 131 | z.state[idx] = NoneToPress 132 | z.cycle[idx] = 0 133 | } else { 134 | z.cycle[idx]++ 135 | } 136 | } else { 137 | z.cycle[idx] = 0 138 | } 139 | case NoneToPress: 140 | z.state[idx] = Press 141 | buf[0] = koebiten.Key(idx) 142 | koebiten.AppendJustPressedKeys(buf) 143 | case Press: 144 | buf[0] = koebiten.Key(idx) 145 | koebiten.AppendPressedKeys(buf) 146 | if current { 147 | z.cycle[idx] = 0 148 | } else { 149 | if z.cycle[idx] >= debounce { 150 | z.state[idx] = PressToRelease 151 | z.cycle[idx] = 0 152 | } else { 153 | z.cycle[idx]++ 154 | } 155 | } 156 | case PressToRelease: 157 | z.state[idx] = None 158 | buf[0] = koebiten.Key(idx) 159 | koebiten.AppendJustReleasedKeys(buf) 160 | } 161 | } 162 | return nil 163 | } 164 | 165 | type Display struct { 166 | d *ili9341.Device 167 | img pixel.Image[pixel.RGB565BE] 168 | } 169 | 170 | func InitDisplay(dev *ili9341.Device, width, height int) *Display { 171 | d := &Display{ 172 | d: dev, 173 | img: pixel.NewImage[pixel.RGB565BE](width*2+1, height*2+1), 174 | } 175 | 176 | ox, oy := d.getImageTopLeftForCentering() 177 | w, h := d.img.Size() 178 | tinydraw.Rectangle(dev, ox-1, oy-1, int16(w)+2, int16(h)+2, white) 179 | 180 | return d 181 | } 182 | 183 | func (d *Display) Size() (x, y int16) { 184 | return 128, 64 185 | } 186 | 187 | func (d *Display) SetPixel(x, y int16, c color.RGBA) { 188 | mx, my := d.Size() 189 | if 0 <= x && x < int16(mx) && 0 <= y && y < int16(my) { 190 | d.img.Set(int(x*2+0), int(y*2+0), pixelWhite) 191 | d.img.Set(int(x*2+1), int(y*2+0), pixelWhite) 192 | d.img.Set(int(x*2+0), int(y*2+1), pixelWhite) 193 | d.img.Set(int(x*2+1), int(y*2+1), pixelWhite) 194 | } 195 | return 196 | cnt := 0 197 | if c.R < 0x80 { 198 | cnt++ 199 | } 200 | if c.G < 0x80 { 201 | cnt++ 202 | } 203 | if c.B < 0x80 { 204 | cnt++ 205 | } 206 | if cnt >= 2 { 207 | d.img.Set(int(x), int(y), pixelWhite) 208 | } else { 209 | d.img.Set(int(x), int(y), pixelBlack) 210 | } 211 | //d.d.SetPixel(x, y, c) 212 | } 213 | 214 | func (d *Display) Display() error { 215 | ox, oy := d.getImageTopLeftForCentering() 216 | return d.d.DrawBitmap(int16(ox), int16(oy), d.img) 217 | } 218 | 219 | func (d *Display) ClearBuffer() { 220 | d.img.FillSolidColor(pixelBlack) 221 | } 222 | 223 | func (d *Display) ClearDisplay() { 224 | } 225 | 226 | func (d *Display) getImageTopLeftForCentering() (int16, int16) { 227 | mx, my := d.img.Size() 228 | ox := (320 - mx) / 2 229 | oy := (240 - my) / 2 230 | return int16(ox), int16(oy) 231 | } 232 | 233 | var ( 234 | white = color.RGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF} 235 | black = color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF} 236 | 237 | pixelWhite = pixel.NewColor[pixel.RGB565BE](0xFF, 0xFF, 0xFF) 238 | pixelBlack = pixel.NewColor[pixel.RGB565BE](0x00, 0x00, 0x00) 239 | ) 240 | -------------------------------------------------------------------------------- /geom.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Hajime Hoshi 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package koebiten 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/chewxy/math32" 21 | ) 22 | 23 | // GeoMDim is a dimension of a GeoM. 24 | const GeoMDim = 3 25 | 26 | // A GeoM represents a matrix to transform geometry when rendering an image. 27 | // 28 | // The initial value is identity. 29 | type GeoM struct { 30 | a_1 float32 // The actual 'a' value minus 1 31 | b float32 32 | c float32 33 | d_1 float32 // The actual 'd' value minus 1 34 | tx float32 35 | ty float32 36 | } 37 | 38 | // String returns a string representation of GeoM. 39 | func (g *GeoM) String() string { 40 | return fmt.Sprintf("[[%f, %f, %f], [%f, %f, %f]]", g.a_1+1, g.b, g.tx, g.c, g.d_1+1, g.ty) 41 | } 42 | 43 | // Reset resets the GeoM as identity. 44 | func (g *GeoM) Reset() { 45 | g.a_1 = 0 46 | g.b = 0 47 | g.c = 0 48 | g.d_1 = 0 49 | g.tx = 0 50 | g.ty = 0 51 | } 52 | 53 | // Apply pre-multiplies a vector (x, y, 1) by the matrix. 54 | // In other words, Apply calculates GeoM * (x, y, 1)^T. 55 | // The return value is x and y values of the result vector. 56 | func (g *GeoM) Apply(x, y float32) (float32, float32) { 57 | return (g.a_1+1)*x + g.b*y + g.tx, g.c*x + (g.d_1+1)*y + g.ty 58 | } 59 | 60 | func (g *GeoM) elements32() (a, b, c, d, tx, ty float32) { 61 | return float32(g.a_1) + 1, float32(g.b), float32(g.c), float32(g.d_1) + 1, float32(g.tx), float32(g.ty) 62 | } 63 | 64 | // Element returns a value of a matrix at (i, j). 65 | func (g *GeoM) Element(i, j int) float32 { 66 | switch { 67 | case i == 0 && j == 0: 68 | return g.a_1 + 1 69 | case i == 0 && j == 1: 70 | return g.b 71 | case i == 0 && j == 2: 72 | return g.tx 73 | case i == 1 && j == 0: 74 | return g.c 75 | case i == 1 && j == 1: 76 | return g.d_1 + 1 77 | case i == 1 && j == 2: 78 | return g.ty 79 | default: 80 | panic("ebiten: i or j is out of index") 81 | } 82 | } 83 | 84 | // Concat multiplies a geometry matrix with the other geometry matrix. 85 | // This is same as multiplying the matrix other and the matrix g in this order. 86 | func (g *GeoM) Concat(other GeoM) { 87 | a := (other.a_1+1)*(g.a_1+1) + other.b*g.c 88 | b := (other.a_1+1)*g.b + other.b*(g.d_1+1) 89 | tx := (other.a_1+1)*g.tx + other.b*g.ty + other.tx 90 | c := other.c*(g.a_1+1) + (other.d_1+1)*g.c 91 | d := other.c*g.b + (other.d_1+1)*(g.d_1+1) 92 | ty := other.c*g.tx + (other.d_1+1)*g.ty + other.ty 93 | 94 | g.a_1 = a - 1 95 | g.b = b 96 | g.c = c 97 | g.d_1 = d - 1 98 | g.tx = tx 99 | g.ty = ty 100 | } 101 | 102 | // Scale scales the matrix by (x, y). 103 | func (g *GeoM) Scale(x, y float32) { 104 | a := (g.a_1 + 1) * x 105 | b := g.b * x 106 | tx := g.tx * x 107 | c := g.c * y 108 | d := (g.d_1 + 1) * y 109 | ty := g.ty * y 110 | 111 | g.a_1 = a - 1 112 | g.b = b 113 | g.c = c 114 | g.d_1 = d - 1 115 | g.tx = tx 116 | g.ty = ty 117 | } 118 | 119 | // Translate translates the matrix by (tx, ty). 120 | func (g *GeoM) Translate(tx, ty float32) { 121 | g.tx += tx 122 | g.ty += ty 123 | } 124 | 125 | // Rotate rotates the matrix clockwise by theta. 126 | // The unit is radian. 127 | func (g *GeoM) Rotate(theta float32) { 128 | if theta == 0 { 129 | return 130 | } 131 | 132 | sin, cos := math32.Sincos(theta) 133 | 134 | a := cos*(g.a_1+1) - sin*g.c 135 | b := cos*g.b - sin*(g.d_1+1) 136 | tx := cos*g.tx - sin*g.ty 137 | c := sin*(g.a_1+1) + cos*g.c 138 | d := sin*g.b + cos*(g.d_1+1) 139 | ty := sin*g.tx + cos*g.ty 140 | 141 | g.a_1 = a - 1 142 | g.b = b 143 | g.c = c 144 | g.d_1 = d - 1 145 | g.tx = tx 146 | g.ty = ty 147 | } 148 | 149 | // Skew skews the matrix by (skewX, skewY). The unit is radian. 150 | func (g *GeoM) Skew(skewX, skewY float32) { 151 | sx := math32.Tan(skewX) 152 | sy := math32.Tan(skewY) 153 | 154 | a := (g.a_1 + 1) + g.c*sx 155 | b := g.b + (g.d_1+1)*sx 156 | c := (g.a_1+1)*sy + g.c 157 | d := g.b*sy + (g.d_1 + 1) 158 | tx := g.tx + g.ty*sx 159 | ty := g.ty + g.tx*sy 160 | 161 | g.a_1 = a - 1 162 | g.b = b 163 | g.c = c 164 | g.d_1 = d - 1 165 | g.tx = tx 166 | g.ty = ty 167 | } 168 | 169 | func (g *GeoM) det2x2() float32 { 170 | return (g.a_1+1)*(g.d_1+1) - g.b*g.c 171 | } 172 | 173 | // IsInvertible returns a boolean value indicating 174 | // whether the matrix g is invertible or not. 175 | func (g *GeoM) IsInvertible() bool { 176 | return g.det2x2() != 0 177 | } 178 | 179 | // Invert inverts the matrix. 180 | // If g is not invertible, Invert panics. 181 | func (g *GeoM) Invert() { 182 | det := g.det2x2() 183 | if det == 0 { 184 | panic("ebiten: g is not invertible") 185 | } 186 | 187 | a := (g.d_1 + 1) / det 188 | b := -g.b / det 189 | c := -g.c / det 190 | d := (g.a_1 + 1) / det 191 | tx := (-(g.d_1+1)*g.tx + g.b*g.ty) / det 192 | ty := (g.c*g.tx + -(g.a_1+1)*g.ty) / det 193 | 194 | g.a_1 = a - 1 195 | g.b = b 196 | g.c = c 197 | g.d_1 = d - 1 198 | g.tx = tx 199 | g.ty = ty 200 | } 201 | 202 | // SetElement sets an element at (i, j). 203 | func (g *GeoM) SetElement(i, j int, element float32) { 204 | e := element 205 | switch { 206 | case i == 0 && j == 0: 207 | g.a_1 = e - 1 208 | case i == 0 && j == 1: 209 | g.b = e 210 | case i == 0 && j == 2: 211 | g.tx = e 212 | case i == 1 && j == 0: 213 | g.c = e 214 | case i == 1 && j == 1: 215 | g.d_1 = e - 1 216 | case i == 1 && j == 2: 217 | g.ty = e 218 | default: 219 | panic("ebiten: i or j is out of index") 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /games/flappygopher/flappygopher/game.go: -------------------------------------------------------------------------------- 1 | package flappygopher 2 | 3 | import ( 4 | "embed" 5 | "math/rand/v2" 6 | 7 | "github.com/sago35/koebiten" 8 | "tinygo.org/x/drivers/pixel" 9 | ) 10 | 11 | type Game struct { 12 | imageGopher *koebiten.Image 13 | imageWall *koebiten.Image 14 | } 15 | 16 | func NewGame() *Game { 17 | game := &Game{} 18 | 19 | game.imageGopher = koebiten.NewImageFromFS(fsys, "gopher.png") 20 | game.imageWall = koebiten.NewImageFromFS(fsys, "wall.png") 21 | 22 | for i := range wallsBuf { 23 | wallsBuf[i] = &wall{} 24 | } 25 | return game 26 | } 27 | 28 | // Game update process 29 | func (game *Game) Update() error { 30 | return nil 31 | } 32 | 33 | // Screen size 34 | func (game *Game) Layout(outsideWidth, outsideHeight int) (w, h int) { 35 | return 128, 64 36 | } 37 | 38 | func (game *Game) Draw(screen *koebiten.Image) { 39 | game.draw() 40 | } 41 | 42 | //go:embed *.png 43 | var fsys embed.FS 44 | 45 | type wall struct { 46 | wallX int 47 | holeY int 48 | } 49 | 50 | var ( 51 | x = float32(20.0) 52 | y = float32(30.0) 53 | vy = float32(0.0) // Velocity of y (速度のy成分) の略 54 | g = float32(0.05) // Gravity (重力加速度) の略 55 | jump = float32(-1.0) // ジャンプ力 56 | 57 | interval = 100 // 壁の追加間隔 58 | intervalMin = 50 // 壁の追加間隔 59 | intervalMax = 100 // 壁の追加間隔 60 | wallStartX = 130 // 壁の初期X座標 61 | walls = []*wall{} // 壁のX座標とY座標 62 | wallsBuf = [8]*wall{} // 壁の実態 63 | wallWidth = 7 // 壁の幅 64 | wallHeight = 128 // 壁の高さ 65 | holeYMax = 48 // 穴のY座標の最大値 66 | holeHeight = 45 // 穴のサイズ(高さ) 67 | holeHeightMin = 40 // 穴のサイズ(高さ) 68 | 69 | gopherWidth = 18 70 | gopherHeight = 23 71 | 72 | scene = "title" 73 | score = 0 74 | ) 75 | 76 | var ( 77 | white = pixel.NewMonochrome(0xFF, 0xFF, 0xFF) 78 | black = pixel.NewMonochrome(0x00, 0x00, 0x00) 79 | ) 80 | 81 | func (game *Game) draw() { 82 | switch scene { 83 | case "title": 84 | game.drawTitle() 85 | case "game": 86 | game.drawGame() 87 | case "gameover": 88 | game.drawGameover() 89 | } 90 | } 91 | 92 | func (game *Game) drawTitle() { 93 | koebiten.Println("click to start") 94 | op := koebiten.DrawImageOptions{} 95 | op.GeoM.Translate(float32(x), float32(y)) 96 | game.imageGopher.DrawImage(nil, op) 97 | 98 | if isAnyKeyJustPressed() { 99 | scene = "game" 100 | } 101 | } 102 | 103 | func (game *Game) drawGame() { 104 | for _, wall := range walls { 105 | if wall.wallX == int(x) { 106 | score += 1 107 | 108 | if score%5 == 0 { 109 | if holeHeightMin <= holeHeight { 110 | holeHeight -= 1 111 | } 112 | } 113 | } 114 | } 115 | koebiten.Println("Score", score) 116 | 117 | if isAnyKeyJustPressed() { 118 | vy = jump 119 | } 120 | vy += g // 速度に加速度を足す 121 | y += vy // 位置に速度を足す 122 | op := koebiten.DrawImageOptions{} 123 | op.GeoM.Translate(float32(x), float32(y)) 124 | game.imageGopher.DrawImage(nil, op) 125 | 126 | // 壁追加処理ここから 127 | interval-- 128 | if interval == 0 { 129 | interval = intervalMin + rand.N(intervalMax-intervalMin) 130 | walls = wallsBuf[:len(walls)+1] 131 | walls[len(walls)-1].wallX = wallStartX 132 | walls[len(walls)-1].holeY = rand.N(holeYMax) 133 | } 134 | // 壁追加処理ここまで 135 | 136 | delete := false 137 | for _, wall := range walls { 138 | wall.wallX -= 1 // 少しずつ左へ 139 | if wall.wallX < 0 { 140 | delete = true 141 | } 142 | } 143 | if delete { 144 | for i := 1; i < len(walls); i++ { 145 | walls[i-1].wallX = walls[i].wallX 146 | walls[i-1].holeY = walls[i].holeY 147 | } 148 | walls = walls[:len(walls)-1] 149 | } 150 | 151 | for _, wall := range walls { 152 | game.drawWalls(wall) 153 | 154 | // gopherくんを表す四角形を作る 155 | aLeft := int(x) 156 | aTop := int(y) 157 | aRight := int(x) + gopherWidth 158 | aBottom := int(y) + gopherHeight 159 | 160 | // 上の壁を表す四角形を作る 161 | bLeft := wall.wallX 162 | bTop := wall.holeY - wallHeight 163 | bRight := wall.wallX + wallWidth 164 | bBottom := wall.holeY 165 | 166 | // 上の壁との当たり判定 167 | if hitTestRects(aLeft, aTop, aRight, aBottom, bLeft, bTop, bRight, bBottom) { 168 | scene = "gameover" 169 | interval = intervalMin 170 | } 171 | 172 | // 下の壁を表す四角形を作る 173 | bLeft = wall.wallX 174 | bTop = wall.holeY + holeHeight 175 | bRight = wall.wallX + wallWidth 176 | bBottom = wall.holeY + holeHeight + wallHeight 177 | 178 | // 下の壁との当たり判定 179 | if hitTestRects(aLeft, aTop, aRight, aBottom, bLeft, bTop, bRight, bBottom) { 180 | scene = "gameover" 181 | interval = intervalMin 182 | } 183 | 184 | if y < 0 { 185 | scene = "gameover" 186 | interval = intervalMin 187 | } 188 | if 64 < y { 189 | scene = "gameover" 190 | interval = intervalMin 191 | } 192 | } 193 | } 194 | 195 | func (game *Game) drawGameover() { 196 | if interval > 0 { 197 | interval-- 198 | op := koebiten.DrawImageOptions{} 199 | op.GeoM.Translate(float32(x), float32(y)) 200 | game.imageGopher.DrawImage(nil, op) 201 | 202 | for _, wall := range walls { 203 | game.drawWalls(wall) 204 | } 205 | } 206 | 207 | koebiten.Println("Game Over") 208 | koebiten.Println("Score", score) 209 | 210 | if interval == 0 && isAnyKeyJustPressed() { 211 | scene = "title" 212 | 213 | x = 20.0 214 | y = 30.0 215 | vy = 0.0 216 | walls = wallsBuf[:0] 217 | score = 0 218 | interval = intervalMax 219 | } 220 | } 221 | 222 | func (game *Game) drawWalls(w *wall) { 223 | if w.wallX < 0-wallWidth || 128+wallWidth < w.wallX { 224 | return 225 | } 226 | // upper wall 227 | op1 := koebiten.DrawImageOptions{} 228 | op1.GeoM.Translate(float32(w.wallX), float32(w.holeY-wallHeight)) 229 | game.imageWall.DrawImage(nil, op1) 230 | 231 | // lower wall 232 | op2 := koebiten.DrawImageOptions{} 233 | op2.GeoM.Translate(float32(w.wallX), float32(w.holeY+holeHeight)) 234 | game.imageWall.DrawImage(nil, op2) 235 | } 236 | 237 | func hitTestRects(aLeft, aTop, aRight, aBottom, bLeft, bTop, bRight, bBottom int) bool { 238 | return aLeft < bRight && 239 | bLeft < aRight && 240 | aTop < bBottom && 241 | bTop < aBottom 242 | } 243 | 244 | func isAnyKeyJustPressed() bool { 245 | return len(koebiten.AppendJustPressedKeys(nil)) > 0 246 | } 247 | -------------------------------------------------------------------------------- /hardware/conf2025badge.go: -------------------------------------------------------------------------------- 1 | //go:build tinygo && conf2025badge 2 | 3 | package hardware 4 | 5 | import ( 6 | "machine" 7 | 8 | "github.com/sago35/koebiten" 9 | "tinygo.org/x/drivers" 10 | "tinygo.org/x/drivers/encoders" 11 | "tinygo.org/x/drivers/ssd1306" 12 | ) 13 | 14 | var Device = CONF2025BADGE{} 15 | 16 | type CONF2025BADGE struct { 17 | } 18 | 19 | func (z CONF2025BADGE) Init() error { 20 | return Init() 21 | } 22 | 23 | func (z CONF2025BADGE) GetDisplay() koebiten.Displayer { 24 | return Display 25 | } 26 | 27 | func (z CONF2025BADGE) KeyUpdate() error { 28 | return keyUpdate() 29 | } 30 | 31 | var ( 32 | Display *ssd1306.Device 33 | ) 34 | 35 | var ( 36 | colPins []machine.Pin 37 | rowPins []machine.Pin 38 | rotaryPins []machine.Pin 39 | gpioPins []machine.Pin 40 | adcPins []ADCDevice 41 | enc *encoders.QuadratureDevice 42 | encOld int 43 | state []State 44 | cycle []int 45 | duration []int 46 | invertRotaryPins = false 47 | keybuf [1]koebiten.Key 48 | ) 49 | 50 | const ( 51 | debounce = 0 52 | ) 53 | 54 | type State uint8 55 | 56 | const ( 57 | None State = iota 58 | NoneToPress 59 | Press 60 | PressToRelease 61 | ) 62 | 63 | type ADCDevice struct { 64 | ADC machine.ADC 65 | PressedFunc func() bool 66 | } 67 | 68 | func (a ADCDevice) Get() bool { 69 | return a.PressedFunc() 70 | } 71 | 72 | func Init() error { 73 | machine.InitADC() 74 | ax := machine.ADC{Pin: machine.GPIO27} 75 | ay := machine.ADC{Pin: machine.GPIO26} 76 | ax.Configure(machine.ADCConfig{}) 77 | ay.Configure(machine.ADCConfig{}) 78 | 79 | adcPins = []ADCDevice{ 80 | {ADC: ax, PressedFunc: func() bool { return ax.Get() < 0x3000 }}, // left 81 | {ADC: ax, PressedFunc: func() bool { return 0xC800 < ax.Get() }}, // right 82 | {ADC: ay, PressedFunc: func() bool { return 0xC800 < ay.Get() }}, // up 83 | {ADC: ay, PressedFunc: func() bool { return ay.Get() < 0x3000 }}, // down 84 | } 85 | 86 | i2c := machine.I2C1 87 | i2c.Configure(machine.I2CConfig{ 88 | Frequency: 2_800_000, 89 | SDA: machine.GPIO6, 90 | SCL: machine.GPIO7, 91 | }) 92 | 93 | d := ssd1306.NewI2C(i2c) 94 | d.Configure(ssd1306.Config{ 95 | Address: 0x3C, 96 | Width: 128, 97 | Height: 64, 98 | }) 99 | d.SetRotation(drivers.Rotation0) 100 | d.ClearDisplay() 101 | Display = d 102 | 103 | gpioPins = []machine.Pin{ 104 | machine.GPIO28, 105 | machine.GPIO29, 106 | machine.GPIO2, // rotary 107 | } 108 | 109 | for _, p := range gpioPins { 110 | p.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) 111 | } 112 | 113 | rotaryPins = []machine.Pin{ 114 | machine.GPIO3, 115 | machine.GPIO4, 116 | } 117 | 118 | if invertRotaryPins { 119 | rotaryPins = []machine.Pin{ 120 | machine.GPIO4, 121 | machine.GPIO3, 122 | } 123 | } 124 | enc = encoders.NewQuadratureViaInterrupt(rotaryPins[0], rotaryPins[1]) 125 | 126 | enc.Configure(encoders.QuadratureConfig{ 127 | Precision: 4, 128 | }) 129 | 130 | state = make([]State, koebiten.KeyDown+1) 131 | cycle = make([]int, koebiten.KeyDown+1) 132 | duration = make([]int, koebiten.KeyDown+1) 133 | 134 | return nil 135 | } 136 | 137 | func keyUpdate() error { 138 | keyGpioUpdate() 139 | keyRotaryUpdate() 140 | keyJoystickUpdate() 141 | return nil 142 | } 143 | 144 | func keyGpioUpdate() { 145 | buf := keybuf[:] 146 | for r := range gpioPins { 147 | current := !gpioPins[r].Get() 148 | idx := r + int(koebiten.Key0) 149 | 150 | switch state[idx] { 151 | case None: 152 | if current { 153 | if cycle[idx] >= debounce { 154 | state[idx] = NoneToPress 155 | cycle[idx] = 0 156 | } else { 157 | cycle[idx]++ 158 | } 159 | } else { 160 | cycle[idx] = 0 161 | } 162 | case NoneToPress: 163 | state[idx] = Press 164 | buf[0] = koebiten.Key(idx) 165 | koebiten.AppendJustPressedKeys(buf) 166 | case Press: 167 | buf[0] = koebiten.Key(idx) 168 | koebiten.AppendPressedKeys(buf) 169 | if current { 170 | cycle[idx] = 0 171 | duration[idx]++ 172 | } else { 173 | if cycle[idx] >= debounce { 174 | state[idx] = PressToRelease 175 | cycle[idx] = 0 176 | duration[idx] = 0 177 | } else { 178 | cycle[idx]++ 179 | } 180 | } 181 | case PressToRelease: 182 | state[idx] = None 183 | buf[0] = koebiten.Key(idx) 184 | koebiten.AppendJustReleasedKeys(buf) 185 | } 186 | } 187 | } 188 | 189 | func keyRotaryUpdate() { 190 | rot := []bool{false, false} 191 | if newValue := enc.Position(); newValue != encOld { 192 | if newValue < encOld { 193 | rot[0] = true 194 | } else { 195 | rot[1] = true 196 | } 197 | encOld = newValue 198 | } 199 | 200 | buf := keybuf[:] 201 | for c, current := range rot { 202 | idx := c + int(koebiten.KeyRotaryLeft) 203 | switch state[idx] { 204 | case None: 205 | if current { 206 | state[idx] = NoneToPress 207 | } else { 208 | } 209 | case NoneToPress: 210 | if current { 211 | state[idx] = Press 212 | } else { 213 | state[idx] = PressToRelease 214 | } 215 | buf[0] = koebiten.Key(idx) 216 | koebiten.AppendJustPressedKeys(buf) 217 | case Press: 218 | buf[0] = koebiten.Key(idx) 219 | koebiten.AppendPressedKeys(buf) 220 | if current { 221 | } else { 222 | state[idx] = PressToRelease 223 | } 224 | case PressToRelease: 225 | if current { 226 | state[idx] = NoneToPress 227 | } else { 228 | state[idx] = None 229 | } 230 | buf[0] = koebiten.Key(idx) 231 | koebiten.AppendJustReleasedKeys(buf) 232 | } 233 | } 234 | } 235 | 236 | func keyJoystickUpdate() { 237 | buf := keybuf[:] 238 | for r, p := range adcPins { 239 | current := p.Get() 240 | idx := r + int(koebiten.KeyLeft) 241 | 242 | switch state[idx] { 243 | case None: 244 | if current { 245 | if cycle[idx] >= debounce { 246 | state[idx] = NoneToPress 247 | cycle[idx] = 0 248 | } else { 249 | cycle[idx]++ 250 | } 251 | } else { 252 | cycle[idx] = 0 253 | } 254 | case NoneToPress: 255 | state[idx] = Press 256 | buf[0] = koebiten.Key(idx) 257 | koebiten.AppendJustPressedKeys(buf) 258 | case Press: 259 | buf[0] = koebiten.Key(idx) 260 | koebiten.AppendPressedKeys(buf) 261 | if current { 262 | cycle[idx] = 0 263 | duration[idx]++ 264 | } else { 265 | if cycle[idx] >= debounce { 266 | state[idx] = PressToRelease 267 | cycle[idx] = 0 268 | duration[idx] = 0 269 | } else { 270 | cycle[idx]++ 271 | } 272 | } 273 | case PressToRelease: 274 | state[idx] = None 275 | buf[0] = koebiten.Key(idx) 276 | koebiten.AppendJustReleasedKeys(buf) 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /games/goradius/goradius/game.go: -------------------------------------------------------------------------------- 1 | package goradius 2 | 3 | import ( 4 | "embed" 5 | "math/rand/v2" 6 | "slices" 7 | 8 | "github.com/sago35/koebiten" 9 | "tinygo.org/x/drivers/pixel" 10 | ) 11 | 12 | //go:embed *.png 13 | var fsys embed.FS 14 | 15 | const ( 16 | gameStateStart = iota 17 | gameStatePlaying 18 | gameStateGameOver 19 | 20 | width = 128 21 | height = 64 22 | 23 | gopherWidth = 20 24 | gopherHeight = 25 25 | 26 | beamMax = 1 27 | beamCooldown = 60 28 | ) 29 | 30 | type Game struct { 31 | gopher *koebiten.Image 32 | x, y int 33 | scale float32 34 | theta float32 35 | score int 36 | beamEnergy int 37 | beamActive bool // ビームが有効かどうかのフラグ 38 | beamCooldownTimer int // ビームのクールダウンタイマー 39 | 40 | gameState int // ゲームの状態を管理する変数 41 | } 42 | 43 | type enemy struct { 44 | enemyX int 45 | enemyY int 46 | speed int // 敵の移動速度 47 | } 48 | 49 | var ( 50 | white = pixel.NewMonochrome(0xFF, 0xFF, 0xFF) 51 | black = pixel.NewMonochrome(0x00, 0x00, 0x00) 52 | 53 | x = float32(20.0) 54 | y = float32(30.0) 55 | 56 | frames = 30 // フレーム数 57 | interval = 120 // 敵の追加間隔 58 | enemie = []*enemy{} // 敵のX座標とY座標 59 | ) 60 | 61 | func NewGame() *Game { 62 | game := &Game{ 63 | gopher: koebiten.NewImageFromFS(fsys, "gopher.png"), 64 | x: width / 2, 65 | y: height / 2, 66 | scale: 1, 67 | beamEnergy: 1, // 初期エネルギーを1に設定 68 | } 69 | return game 70 | } 71 | 72 | // Game update process 73 | func (g *Game) Update() error { 74 | ds := float32(0.05) 75 | dt := float32(0.2) 76 | speed := 1 77 | dx := 1 * speed 78 | dy := 1 * speed 79 | 80 | // スタート画面からゲームプレイ画面に遷移 81 | if g.gameState != gameStatePlaying && koebiten.IsKeyJustPressed(koebiten.Key0) { 82 | // スコアのリセット 83 | g.score = 0 84 | g.gameState = gameStatePlaying 85 | g.x = width / 2 86 | g.y = height / 2 87 | g.scale = 1 88 | g.theta = 0 89 | g.beamEnergy = 1 90 | enemie = enemie[:0] 91 | } 92 | 93 | // rotary buttonを回すとgopherが回転する 94 | // キーボードを押しながら回すと拡大縮小する 95 | if koebiten.IsKeyPressed(koebiten.KeyRotaryRight) { 96 | if isAnyKeyboardKeyPressed() { 97 | g.scale += ds 98 | } else { 99 | g.theta += dt 100 | } 101 | } 102 | if koebiten.IsKeyPressed(koebiten.KeyRotaryLeft) { 103 | if isAnyKeyboardKeyPressed() { 104 | g.scale -= ds 105 | } else { 106 | g.theta -= dt 107 | } 108 | } 109 | 110 | // joystickを倒すとgopherが移動する 111 | if koebiten.IsKeyPressed(koebiten.KeyArrowRight) { 112 | if g.x < width { 113 | g.x += dx 114 | } 115 | } 116 | if koebiten.IsKeyPressed(koebiten.KeyArrowLeft) { 117 | if g.x > -5 { 118 | g.x -= dx 119 | } 120 | } 121 | if koebiten.IsKeyPressed(koebiten.KeyArrowDown) { 122 | if g.y <= height { 123 | g.y += dy 124 | } 125 | } 126 | if koebiten.IsKeyPressed(koebiten.KeyArrowUp) { 127 | if g.y > -5 { 128 | g.y -= dy 129 | } 130 | } 131 | 132 | // ビームを発射する 133 | if g.beamCooldownTimer > 0 { 134 | // クールダウン中はタイマーを減らす 135 | g.beamCooldownTimer-- 136 | g.beamActive = false 137 | // クールダウンが終わったらエネルギーを1に回復 138 | if g.beamCooldownTimer == 0 { 139 | g.beamEnergy = 1 140 | } 141 | } else if koebiten.IsKeyPressed(koebiten.Key1) && g.beamEnergy > 0 { 142 | // エネルギーがある場合のみビーム発射 143 | g.beamActive = true 144 | g.beamEnergy-- 145 | // エネルギーが0になったらクールダウン開始 146 | if g.beamEnergy <= 0 { 147 | g.beamCooldownTimer = beamCooldown 148 | } 149 | } else { 150 | // Key1が押されていない場合またはエネルギーがない場合はビームを無効化 151 | g.beamActive = false 152 | } 153 | 154 | // 敵の移動 155 | for i := 0; i < len(enemie); i++ { 156 | if i < len(enemie) { // 境界チェック 157 | enemie[i].enemyX -= 1 // 少しずつ左へ 158 | 159 | // 画面外に出た敵を削除 160 | if enemie[i].enemyX < -gopherWidth { 161 | enemie = append(enemie[:i], enemie[i+1:]...) 162 | i-- 163 | } 164 | } 165 | } 166 | 167 | return nil 168 | } 169 | 170 | func (g *Game) drawEnemy(e *enemy) { 171 | // 敵を描画する 172 | koebiten.DrawImageFS(nil, fsys, "enemy.png", e.enemyX, e.enemyY) 173 | // 自機との当たり判定 174 | if hitRects(g.x, g.y, g.x+gopherWidth, g.y+gopherHeight, 175 | e.enemyX, e.enemyY, e.enemyX+gopherWidth, e.enemyY+gopherHeight) { 176 | // 当たった場合、ゲームオーバー 177 | g.gameState = gameStateGameOver 178 | } 179 | } 180 | 181 | func (g *Game) drawTitle() { 182 | // タイトル画面を描画する 183 | koebiten.Println("Goradius") 184 | koebiten.Println("Press key to start") 185 | } 186 | 187 | func (g *Game) drawGameOver() { 188 | // スコアを表示する 189 | koebiten.Println("Score:", g.score) 190 | // ゲームオーバー画面を描画する 191 | koebiten.Println("Game Over") 192 | } 193 | 194 | func (g *Game) drawGame(screen *koebiten.Image) { 195 | // 自機描画 196 | op := koebiten.DrawImageOptions{} 197 | op.GeoM.Translate(-float32(gopherWidth)/2, -float32(gopherHeight)/2) 198 | op.GeoM.Scale(g.scale, g.scale) 199 | op.GeoM.Rotate(g.theta) 200 | op.GeoM.Translate(float32(g.x), float32(g.y)) 201 | g.gopher.DrawImage(screen, op) 202 | 203 | // スコアとエネルギー表示する 204 | koebiten.Println("beam:", g.beamEnergy) 205 | koebiten.Println("Score:", g.score) 206 | 207 | // 一定間隔で敵を追加 208 | frames++ 209 | if rand.N(interval) < 3 { 210 | enemyY := rand.N(height - gopherHeight) 211 | // スピードもランダムに(1〜3の範囲) 212 | enemySpeed := rand.N(3) + 1 213 | enemy := &enemy{width, enemyY, enemySpeed} 214 | enemie = append(enemie, enemy) 215 | } 216 | 217 | // 敵の描画 218 | for i := 0; i < len(enemie); i++ { 219 | if i < len(enemie) { // 境界チェック 220 | g.drawEnemy(enemie[i]) 221 | } 222 | } 223 | 224 | // ビームを描画(ビームが有効な場合) 225 | if g.beamActive && g.beamEnergy <= beamMax { 226 | for i := 0; i < 10; i++ { 227 | beamX := g.x + (i * 10) 228 | beamY := g.y 229 | koebiten.DrawImageFS(nil, fsys, "beam.png", beamX, beamY) 230 | 231 | // 各ビームセグメントと敵の当たり判定 232 | for j := 0; j < len(enemie); j++ { 233 | if j < len(enemie) { // 配列の境界チェック 234 | if hitBeam(beamX, beamY, beamX+gopherWidth, beamY+gopherHeight, 235 | enemie[j].enemyX, enemie[j].enemyY, enemie[j].enemyX+gopherWidth, enemie[j].enemyY+gopherHeight) { 236 | 237 | // 敵が当たった場合、敵を削除して得点加算 238 | g.score++ 239 | 240 | // 敵をスライスから削除 241 | enemie = append(enemie[:j], enemie[j+1:]...) 242 | j-- // 削除したので、インデックスを1つ戻す 243 | break 244 | } 245 | } 246 | } 247 | } 248 | } 249 | } 250 | 251 | // Screen size 252 | func (g *Game) Layout(outsideWidth, outsideHeight int) (w, h int) { 253 | return width, height 254 | } 255 | 256 | func (g *Game) Draw(screen *koebiten.Image) { 257 | switch g.gameState { 258 | case gameStateStart: 259 | g.drawTitle() 260 | case gameStatePlaying: 261 | g.drawGame(screen) 262 | case gameStateGameOver: 263 | g.drawGameOver() 264 | default: 265 | g.drawTitle() 266 | } 267 | } 268 | 269 | // isAnyKeyboardKeyPressed returns true if any keyboard key is pressed 270 | // 271 | // keyboard key are koebiten.Key0 to koebiten.Key11 272 | func isAnyKeyboardKeyPressed() bool { 273 | return slices.ContainsFunc(koebiten.AppendPressedKeys(nil), func(k koebiten.Key) bool { 274 | switch k { 275 | case 276 | koebiten.Key0, 277 | koebiten.Key1, 278 | koebiten.Key2, 279 | koebiten.Key3, 280 | koebiten.Key4, 281 | koebiten.Key5, 282 | koebiten.Key6, 283 | koebiten.Key7, 284 | koebiten.Key8, 285 | koebiten.Key9, 286 | koebiten.Key10, 287 | koebiten.Key11: 288 | return true 289 | default: 290 | return false 291 | } 292 | }) 293 | } 294 | 295 | // キャラと敵の当たり判定で、場所が重なっているかどうかを判定する 296 | func hitRects(aLeft, aTop, aRight, aBottom, bLeft, bTop, bRight, bBottom int) bool { 297 | return aTop < bBottom && bTop < aBottom && aLeft < bRight && bLeft < aRight 298 | } 299 | 300 | func hitBeam(aLeft, aTop, aRight, aBottom, bLeft, bTop, bRight, bBottom int) bool { 301 | return aTop < bBottom && bTop < aBottom && aLeft < bRight && bLeft < aRight 302 | } 303 | -------------------------------------------------------------------------------- /koebiten.go: -------------------------------------------------------------------------------- 1 | //go:build tinygo 2 | 3 | package koebiten 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "image/color" 9 | "io/fs" 10 | "reflect" 11 | "strings" 12 | "time" 13 | 14 | "github.com/chewxy/math32" 15 | "tinygo.org/x/drivers" 16 | "tinygo.org/x/drivers/image/png" 17 | "tinygo.org/x/drivers/pixel" 18 | "tinygo.org/x/tinydraw" 19 | "tinygo.org/x/tinyfont" 20 | ) 21 | 22 | // Displayer interface for display operations. 23 | type Displayer interface { 24 | drivers.Displayer 25 | ClearDisplay() 26 | ClearBuffer() 27 | } 28 | 29 | var ( 30 | display Displayer 31 | 32 | textY int16 33 | ticks uint32 34 | tickTimes [32]uint32 35 | enableBenchmark bool 36 | ) 37 | 38 | var ( 39 | white = color.RGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF} 40 | black = color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF} 41 | ) 42 | 43 | var keyUpdate = func() error { return nil } 44 | 45 | func init() { 46 | pngBuffer = map[string]pixel.Image[pixel.Monochrome]{} 47 | } 48 | 49 | // Run starts the main loop for the application. 50 | func Run(d func()) error { 51 | return RunGame(dummyGame(d)) 52 | } 53 | 54 | func RunGame(game Game) error { 55 | tick := time.Tick(32 * time.Millisecond) 56 | for { 57 | <-tick 58 | ticks++ 59 | if enableBenchmark && (ticks%32) == 0 { 60 | // print per 32 frame 61 | min := uint32(0xFFFFFFFF) 62 | max := uint32(0x00000000) 63 | for _, t := range tickTimes { 64 | if t < min { 65 | min = t 66 | } 67 | if max < t { 68 | max = t 69 | } 70 | fmt.Printf("%02d,", t/320) 71 | } 72 | fmt.Printf(" %3d %% - %3d %%\n", min/320, max/320) 73 | } 74 | s := time.Now().UnixMicro() 75 | 76 | keyUpdate() 77 | theInputState.update() 78 | textY = 0 79 | display.ClearBuffer() 80 | err := game.Update() 81 | if err != nil { 82 | if errors.Is(err, Termination) { 83 | return nil 84 | } 85 | return err 86 | } 87 | game.Draw(nil) 88 | display.Display() 89 | tickTimes[ticks%32] = uint32(time.Now().UnixMicro() - s) 90 | } 91 | return nil 92 | } 93 | 94 | // SetWindowSize sets the size of the display window. 95 | func SetWindowSize(w, h int) { 96 | } 97 | 98 | // SetWindowTitle sets the title of the display window. 99 | func SetWindowTitle(title string) { 100 | } 101 | 102 | func SetHardware(h Hardware) error { 103 | err := h.Init() 104 | if err != nil { 105 | return err 106 | } 107 | display = h.GetDisplay() 108 | keyUpdate = h.KeyUpdate 109 | return nil 110 | } 111 | 112 | // SetRotation sets the display rotation mode. 113 | // If the display is already a RotatedDisplay, it updates the mode. 114 | // Otherwise, it wraps the existing display in a new RotatedDisplay with the specified mode. 115 | func SetRotation(mode int) { 116 | d, ok := display.(*RotatedDisplay) 117 | if ok { 118 | d.mode = mode 119 | } else { 120 | display = &RotatedDisplay{ 121 | Displayer: display, 122 | mode: mode, 123 | } 124 | } 125 | } 126 | 127 | // Println prints formatted output to the display. 128 | func Println(args ...any) { 129 | str := []string{} 130 | for _, x := range args { 131 | s, ok := x.(string) 132 | if ok { 133 | str = append(str, s) 134 | continue 135 | } 136 | 137 | i, ok := x.(int) 138 | if ok { 139 | str = append(str, fmt.Sprintf("%d", i)) 140 | continue 141 | } 142 | } 143 | 144 | textY += 8 145 | tinyfont.WriteLine(display, &tinyfont.Org01, 2, textY, strings.Join(str, " "), white) 146 | } 147 | 148 | // DrawText draws text on the display. 149 | func DrawText(dst Displayer, str string, font tinyfont.Fonter, x, y int16, c pixel.BaseColor) { 150 | if isNil(dst) { 151 | dst = display 152 | } 153 | if font == nil { 154 | font = &tinyfont.Org01 155 | } 156 | tinyfont.WriteLine(dst, font, x, y, str, c.RGBA()) 157 | } 158 | 159 | // DrawRect draws a rectangle on the display. 160 | func DrawRect(dst Displayer, x, y, w, h int, c pixel.BaseColor) { 161 | if isNil(dst) { 162 | dst = display 163 | } 164 | tinydraw.Rectangle(dst, int16(x), int16(y), int16(w), int16(h), c.RGBA()) 165 | } 166 | 167 | // DrawFilledRect draws a filled rectangle on the display. 168 | func DrawFilledRect(dst Displayer, x, y, w, h int, c pixel.BaseColor) { 169 | if isNil(dst) { 170 | dst = display 171 | } 172 | tinydraw.FilledRectangle(dst, int16(x), int16(y), int16(w), int16(h), c.RGBA()) 173 | } 174 | 175 | // DrawLine draws a line on the display. 176 | func DrawLine(dst Displayer, x1, y1, x2, y2 int, c pixel.BaseColor) { 177 | if isNil(dst) { 178 | dst = display 179 | } 180 | tinydraw.Line(dst, int16(x1), int16(y1), int16(x2), int16(y2), c.RGBA()) 181 | } 182 | 183 | // DrawCircle draws a circle on the display. 184 | func DrawCircle(dst Displayer, x, y, r int, c pixel.BaseColor) { 185 | if isNil(dst) { 186 | dst = display 187 | } 188 | tinydraw.Circle(dst, int16(x), int16(y), int16(r), c.RGBA()) 189 | } 190 | 191 | // DrawFilledCircle draws a filled circle on the display. 192 | func DrawFilledCircle(dst Displayer, x, y, r int, c pixel.BaseColor) { 193 | if isNil(dst) { 194 | dst = display 195 | } 196 | tinydraw.FilledCircle(dst, int16(x), int16(y), int16(r), c.RGBA()) 197 | } 198 | 199 | // DrawTriangle draws a triangle on the display. 200 | func DrawTriangle(dst Displayer, x0, y0, x1, y1, x2, y2 int, c pixel.BaseColor) { 201 | if isNil(dst) { 202 | dst = display 203 | } 204 | tinydraw.Triangle(dst, int16(x0), int16(y0), int16(x1), int16(y1), int16(x2), int16(y2), c.RGBA()) 205 | } 206 | 207 | // DrawFilledTriangle draws a filled triangle on the display. 208 | func DrawFilledTriangle(dst Displayer, x0, y0, x1, y1, x2, y2 int, c pixel.BaseColor) { 209 | if isNil(dst) { 210 | dst = display 211 | } 212 | tinydraw.FilledTriangle(dst, int16(x0), int16(y0), int16(x1), int16(y1), int16(x2), int16(y2), c.RGBA()) 213 | } 214 | 215 | var ( 216 | buffer [3 * 8 * 8 * 4]uint16 217 | pngBuffer map[string]pixel.Image[pixel.Monochrome] 218 | ) 219 | 220 | type DrawImageFSOptions struct { 221 | GeoM GeoM 222 | } 223 | 224 | // DrawImageFS draws an image from the filesystem onto the display. 225 | // 226 | // Deprecated: Use Image and Image.DrawImage instead. 227 | func DrawImageFS(dst Displayer, fsys fs.FS, path string, x, y int) { 228 | op := DrawImageFSOptions{} 229 | op.GeoM.Translate(float32(x), float32(y)) 230 | DrawImageFSWithOptions(dst, fsys, path, op) 231 | } 232 | 233 | // DrawImageFSWithOptions draws an image from the filesystem onto the display with options. 234 | // 235 | // Deprecated: Use Image and Image.DrawImage instead. 236 | func DrawImageFSWithOptions(dst Displayer, fsys fs.FS, path string, options DrawImageFSOptions) { 237 | if isNil(dst) { 238 | dst = display 239 | } 240 | img, ok := pngBuffer[path] 241 | if !ok { 242 | p, err := fsys.Open(path) 243 | if err != nil { 244 | return 245 | } 246 | 247 | png.SetCallback(buffer[:], func(data []uint16, x, y, w, h, width, height int16) { 248 | if img.Len() == 0 { 249 | img = pixel.NewImage[pixel.Monochrome](int(width), int(height)) 250 | } 251 | 252 | for yy := int16(0); yy < h; yy++ { 253 | for xx := int16(0); xx < w; xx++ { 254 | c := C565toRGBA(data[yy*w+xx]) 255 | cnt := 0 256 | if c.R < 0x80 { 257 | cnt++ 258 | } 259 | if c.G < 0x80 { 260 | cnt++ 261 | } 262 | if c.B < 0x80 { 263 | cnt++ 264 | } 265 | if cnt >= 2 { 266 | img.Set(int(x+xx), int(y+yy), true) 267 | } 268 | } 269 | } 270 | }) 271 | 272 | _, err = png.Decode(p) 273 | if err != nil { 274 | return 275 | } 276 | pngBuffer[path] = img 277 | } 278 | 279 | geoM := options.GeoM 280 | if !geoM.IsInvertible() { 281 | return 282 | } 283 | 284 | w, h := img.Size() 285 | if geoM.a_1 == 0 && geoM.b == 0 && geoM.c == 0 && geoM.d_1 == 0 { 286 | tx, ty := geoM.Apply(0, 0) 287 | ox, oy := int(math32.Round(tx)), int(math32.Round(ty)) 288 | for yy := 0; yy < h; yy++ { 289 | for xx := 0; xx < w; xx++ { 290 | if img.Get(xx, yy) == true { 291 | dst.SetPixel(int16(xx+ox), int16(yy+oy), white) 292 | } 293 | } 294 | } 295 | } else { 296 | for yy := 0; yy < h; yy++ { 297 | for xx := 0; xx < w; xx++ { 298 | if img.Get(xx, yy) == true { 299 | xxf, yyf := geoM.Apply(float32(xx), float32(yy)) 300 | dst.SetPixel(int16(math32.Round(xxf)), int16(math32.Round(yyf)), white) 301 | } 302 | } 303 | } 304 | } 305 | } 306 | 307 | func isNil(d Displayer) bool { 308 | return d == nil || (reflect.ValueOf(d).Kind() == reflect.Ptr && reflect.ValueOf(d).IsNil()) 309 | } 310 | -------------------------------------------------------------------------------- /hardware/zero-kb02.go: -------------------------------------------------------------------------------- 1 | //go:build tinygo && zero_kb02 2 | 3 | package hardware 4 | 5 | import ( 6 | "machine" 7 | 8 | "github.com/sago35/koebiten" 9 | "tinygo.org/x/drivers" 10 | "tinygo.org/x/drivers/encoders" 11 | "tinygo.org/x/drivers/ssd1306" 12 | ) 13 | 14 | var Device = ZERO_KB02{} 15 | 16 | type ZERO_KB02 struct { 17 | } 18 | 19 | func (z ZERO_KB02) Init() error { 20 | return Init() 21 | } 22 | 23 | func (z ZERO_KB02) GetDisplay() koebiten.Displayer { 24 | return Display 25 | } 26 | 27 | func (z ZERO_KB02) KeyUpdate() error { 28 | return keyUpdate() 29 | } 30 | 31 | var ( 32 | Display *ssd1306.Device 33 | ) 34 | 35 | var ( 36 | colPins []machine.Pin 37 | rowPins []machine.Pin 38 | rotaryPins []machine.Pin 39 | gpioPins []machine.Pin 40 | adcPins []ADCDevice 41 | enc *encoders.QuadratureDevice 42 | encOld int 43 | state []State 44 | cycle []int 45 | duration []int 46 | invertRotaryPins = false 47 | keybuf [1]koebiten.Key 48 | ) 49 | 50 | const ( 51 | debounce = 0 52 | ) 53 | 54 | type State uint8 55 | 56 | const ( 57 | None State = iota 58 | NoneToPress 59 | Press 60 | PressToRelease 61 | ) 62 | 63 | type ADCDevice struct { 64 | ADC machine.ADC 65 | PressedFunc func() bool 66 | } 67 | 68 | func (a ADCDevice) Get() bool { 69 | return a.PressedFunc() 70 | } 71 | 72 | func Init() error { 73 | machine.InitADC() 74 | ax := machine.ADC{Pin: machine.GPIO29} 75 | ay := machine.ADC{Pin: machine.GPIO28} 76 | ax.Configure(machine.ADCConfig{}) 77 | ay.Configure(machine.ADCConfig{}) 78 | 79 | adcPins = []ADCDevice{ 80 | {ADC: ax, PressedFunc: func() bool { return ax.Get() < 0x4800 }}, // left 81 | {ADC: ax, PressedFunc: func() bool { return 0xB800 < ax.Get() }}, // right 82 | {ADC: ay, PressedFunc: func() bool { return 0xB800 < ay.Get() }}, // up 83 | {ADC: ay, PressedFunc: func() bool { return ay.Get() < 0x4800 }}, // down 84 | } 85 | 86 | i2c := machine.I2C0 87 | i2c.Configure(machine.I2CConfig{ 88 | Frequency: 2_800_000, 89 | SDA: machine.GPIO12, 90 | SCL: machine.GPIO13, 91 | }) 92 | 93 | d := ssd1306.NewI2C(i2c) 94 | d.Configure(ssd1306.Config{ 95 | Address: 0x3C, 96 | Width: 128, 97 | Height: 64, 98 | }) 99 | d.SetRotation(drivers.Rotation180) 100 | d.ClearDisplay() 101 | Display = d 102 | 103 | gpioPins = []machine.Pin{ 104 | machine.GPIO2, // rotary 105 | machine.GPIO0, // joystick 106 | } 107 | 108 | for _, p := range gpioPins { 109 | p.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) 110 | } 111 | 112 | colPins = []machine.Pin{ 113 | machine.GPIO5, 114 | machine.GPIO6, 115 | machine.GPIO7, 116 | machine.GPIO8, 117 | } 118 | 119 | rowPins = []machine.Pin{ 120 | machine.GPIO9, 121 | machine.GPIO10, 122 | machine.GPIO11, 123 | } 124 | 125 | for _, c := range colPins { 126 | c.Configure(machine.PinConfig{Mode: machine.PinOutput}) 127 | c.Low() 128 | } 129 | 130 | for _, c := range rowPins { 131 | c.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) 132 | } 133 | 134 | rotaryPins = []machine.Pin{ 135 | machine.GPIO3, 136 | machine.GPIO4, 137 | } 138 | 139 | if invertRotaryPins { 140 | rotaryPins = []machine.Pin{ 141 | machine.GPIO4, 142 | machine.GPIO3, 143 | } 144 | } 145 | enc = encoders.NewQuadratureViaInterrupt(rotaryPins[0], rotaryPins[1]) 146 | 147 | enc.Configure(encoders.QuadratureConfig{ 148 | Precision: 4, 149 | }) 150 | 151 | state = make([]State, len(colPins)*len(rowPins)+len(gpioPins)+len(rotaryPins)+len(adcPins)) 152 | cycle = make([]int, len(colPins)*len(rowPins)+len(gpioPins)+len(rotaryPins)+len(adcPins)) 153 | duration = make([]int, len(colPins)*len(rowPins)+len(gpioPins)+len(rotaryPins)+len(adcPins)) 154 | 155 | return nil 156 | } 157 | 158 | func keyUpdate() error { 159 | keyGpioUpdate() 160 | keyRotaryUpdate() 161 | keyMatrixUpdate() 162 | keyJoystickUpdate() 163 | return nil 164 | } 165 | 166 | func keyGpioUpdate() { 167 | buf := keybuf[:] 168 | for r := range gpioPins { 169 | current := !gpioPins[r].Get() 170 | idx := r + len(colPins)*len(rowPins) 171 | 172 | switch state[idx] { 173 | case None: 174 | if current { 175 | if cycle[idx] >= debounce { 176 | state[idx] = NoneToPress 177 | cycle[idx] = 0 178 | } else { 179 | cycle[idx]++ 180 | } 181 | } else { 182 | cycle[idx] = 0 183 | } 184 | case NoneToPress: 185 | state[idx] = Press 186 | buf[0] = koebiten.Key(idx) 187 | koebiten.AppendJustPressedKeys(buf) 188 | case Press: 189 | buf[0] = koebiten.Key(idx) 190 | koebiten.AppendPressedKeys(buf) 191 | if current { 192 | cycle[idx] = 0 193 | duration[idx]++ 194 | } else { 195 | if cycle[idx] >= debounce { 196 | state[idx] = PressToRelease 197 | cycle[idx] = 0 198 | duration[idx] = 0 199 | } else { 200 | cycle[idx]++ 201 | } 202 | } 203 | case PressToRelease: 204 | state[idx] = None 205 | buf[0] = koebiten.Key(idx) 206 | koebiten.AppendJustReleasedKeys(buf) 207 | } 208 | } 209 | } 210 | 211 | func keyRotaryUpdate() { 212 | rot := []bool{false, false} 213 | if newValue := enc.Position(); newValue != encOld { 214 | if newValue < encOld { 215 | rot[0] = true 216 | } else { 217 | rot[1] = true 218 | } 219 | encOld = newValue 220 | } 221 | 222 | buf := keybuf[:] 223 | for c, current := range rot { 224 | idx := c + len(colPins)*len(rowPins) + 2 225 | switch state[idx] { 226 | case None: 227 | if current { 228 | state[idx] = NoneToPress 229 | } else { 230 | } 231 | case NoneToPress: 232 | if current { 233 | state[idx] = Press 234 | } else { 235 | state[idx] = PressToRelease 236 | } 237 | buf[0] = koebiten.Key(idx) 238 | koebiten.AppendJustPressedKeys(buf) 239 | case Press: 240 | buf[0] = koebiten.Key(idx) 241 | koebiten.AppendPressedKeys(buf) 242 | if current { 243 | } else { 244 | state[idx] = PressToRelease 245 | } 246 | case PressToRelease: 247 | if current { 248 | state[idx] = NoneToPress 249 | } else { 250 | state[idx] = None 251 | } 252 | buf[0] = koebiten.Key(idx) 253 | koebiten.AppendJustReleasedKeys(buf) 254 | } 255 | } 256 | } 257 | 258 | func keyMatrixUpdate() { 259 | buf := keybuf[:] 260 | for c := range colPins { 261 | for r := range rowPins { 262 | colPins[c].Configure(machine.PinConfig{Mode: machine.PinOutput}) 263 | colPins[c].High() 264 | current := rowPins[r].Get() 265 | idx := r*len(colPins) + c 266 | 267 | switch state[idx] { 268 | case None: 269 | if current { 270 | if cycle[idx] >= debounce { 271 | state[idx] = NoneToPress 272 | cycle[idx] = 0 273 | } else { 274 | cycle[idx]++ 275 | } 276 | } else { 277 | cycle[idx] = 0 278 | } 279 | case NoneToPress: 280 | state[idx] = Press 281 | buf[0] = koebiten.Key(idx) 282 | koebiten.AppendJustPressedKeys(buf) 283 | case Press: 284 | buf[0] = koebiten.Key(idx) 285 | koebiten.AppendPressedKeys(buf) 286 | if current { 287 | cycle[idx] = 0 288 | duration[idx]++ 289 | } else { 290 | if cycle[idx] >= debounce { 291 | state[idx] = PressToRelease 292 | cycle[idx] = 0 293 | duration[idx] = 0 294 | } else { 295 | cycle[idx]++ 296 | } 297 | } 298 | case PressToRelease: 299 | state[idx] = None 300 | buf[0] = koebiten.Key(idx) 301 | koebiten.AppendJustReleasedKeys(buf) 302 | } 303 | 304 | colPins[c].Low() 305 | colPins[c].Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) 306 | } 307 | } 308 | } 309 | 310 | func keyJoystickUpdate() { 311 | buf := keybuf[:] 312 | for r, p := range adcPins { 313 | current := p.Get() 314 | idx := r + len(colPins)*len(rowPins) + len(gpioPins) + len(rotaryPins) 315 | 316 | switch state[idx] { 317 | case None: 318 | if current { 319 | if cycle[idx] >= debounce { 320 | state[idx] = NoneToPress 321 | cycle[idx] = 0 322 | } else { 323 | cycle[idx]++ 324 | } 325 | } else { 326 | cycle[idx] = 0 327 | } 328 | case NoneToPress: 329 | state[idx] = Press 330 | buf[0] = koebiten.Key(idx) 331 | koebiten.AppendJustPressedKeys(buf) 332 | case Press: 333 | buf[0] = koebiten.Key(idx) 334 | koebiten.AppendPressedKeys(buf) 335 | if current { 336 | cycle[idx] = 0 337 | duration[idx]++ 338 | } else { 339 | if cycle[idx] >= debounce { 340 | state[idx] = PressToRelease 341 | cycle[idx] = 0 342 | duration[idx] = 0 343 | } else { 344 | cycle[idx]++ 345 | } 346 | } 347 | case PressToRelease: 348 | state[idx] = None 349 | buf[0] = koebiten.Key(idx) 350 | koebiten.AppendJustReleasedKeys(buf) 351 | } 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /games/blocks/blocks/blocks.go: -------------------------------------------------------------------------------- 1 | package blocks 2 | 3 | import ( 4 | "math/rand" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/sago35/koebiten" 9 | "tinygo.org/x/drivers/pixel" 10 | "tinygo.org/x/tinyfont" 11 | ) 12 | 13 | var ( 14 | backgroundColor = pixel.NewMonochrome(0x00, 0x00, 0x00) 15 | frameColor = pixel.NewMonochrome(0xFF, 0xFF, 0xFF) 16 | ) 17 | 18 | const ( 19 | scale = 1 20 | screenWidth = 64 * scale 21 | screenHeight = 128 * scale 22 | gridSize = 5 * scale 23 | ) 24 | 25 | var ( 26 | dropInterval = time.Duration(1000 * time.Millisecond) 27 | lastDropTime = time.Now() 28 | moveInterval = time.Duration(100 * time.Millisecond) 29 | lastMoveTime = time.Now() 30 | lastMoveAction = time.Duration(0) 31 | ) 32 | 33 | type Game struct { 34 | board [12][24]int // Game board 35 | tetromino Tetromino // Current block 36 | score int // Score 37 | highScore int // High Score 38 | scene string 39 | next Tetromino 40 | } 41 | 42 | type Tetromino struct { 43 | shapes [][][]int // Holds 4 rotation states 44 | rotation int // Holds the current rotation state 45 | x, y int // Position 46 | } 47 | 48 | var ( 49 | // 4 rotation states for T-shaped tetromino 50 | tShapes = [][][]int{ 51 | { 52 | {0, 1, 0}, 53 | {1, 1, 1}, 54 | {0, 0, 0}, 55 | }, 56 | { 57 | {0, 1, 0}, 58 | {0, 1, 1}, 59 | {0, 1, 0}, 60 | }, 61 | { 62 | {0, 0, 0}, 63 | {1, 1, 1}, 64 | {0, 1, 0}, 65 | }, 66 | { 67 | {0, 1, 0}, 68 | {1, 1, 0}, 69 | {0, 1, 0}, 70 | }, 71 | } 72 | 73 | // Other tetrominos also have rotation states 74 | // Examples: I-shaped, O-shaped, L-shaped, J-shaped, S-shaped, Z-shaped, etc. 75 | iShapes = [][][]int{ 76 | { 77 | {0, 0, 0, 0}, 78 | {1, 1, 1, 1}, 79 | {0, 0, 0, 0}, 80 | {0, 0, 0, 0}, 81 | }, 82 | { 83 | {0, 0, 1, 0}, 84 | {0, 0, 1, 0}, 85 | {0, 0, 1, 0}, 86 | {0, 0, 1, 0}, 87 | }, 88 | { 89 | {0, 0, 0, 0}, 90 | {0, 0, 0, 0}, 91 | {1, 1, 1, 1}, 92 | {0, 0, 0, 0}, 93 | }, 94 | { 95 | {0, 1, 0, 0}, 96 | {0, 1, 0, 0}, 97 | {0, 1, 0, 0}, 98 | {0, 1, 0, 0}, 99 | }, 100 | } 101 | 102 | oShapes = [][][]int{ 103 | { 104 | {1, 1}, 105 | {1, 1}, 106 | }, 107 | } 108 | 109 | lShapes = [][][]int{ 110 | { 111 | {0, 0, 0}, 112 | {1, 1, 1}, 113 | {1, 0, 0}, 114 | }, 115 | { 116 | {1, 1, 0}, 117 | {0, 1, 0}, 118 | {0, 1, 0}, 119 | }, 120 | { 121 | {0, 0, 1}, 122 | {1, 1, 1}, 123 | {0, 0, 0}, 124 | }, 125 | { 126 | {0, 1, 0}, 127 | {0, 1, 0}, 128 | {0, 1, 1}, 129 | }, 130 | } 131 | 132 | jShapes = [][][]int{ 133 | { 134 | {0, 0, 0}, 135 | {1, 1, 1}, 136 | {0, 0, 1}, 137 | }, 138 | { 139 | {0, 1, 0}, 140 | {0, 1, 0}, 141 | {1, 1, 0}, 142 | }, 143 | { 144 | {1, 0, 0}, 145 | {1, 1, 1}, 146 | {0, 0, 0}, 147 | }, 148 | { 149 | {0, 1, 1}, 150 | {0, 1, 0}, 151 | {0, 1, 0}, 152 | }, 153 | } 154 | 155 | sShapes = [][][]int{ 156 | { 157 | {0, 0, 0}, 158 | {0, 1, 1}, 159 | {1, 1, 0}, 160 | }, 161 | { 162 | {1, 0, 0}, 163 | {1, 1, 0}, 164 | {0, 1, 0}, 165 | }, 166 | { 167 | {0, 1, 1}, 168 | {1, 1, 0}, 169 | {0, 0, 0}, 170 | }, 171 | { 172 | {0, 1, 0}, 173 | {0, 1, 1}, 174 | {0, 0, 1}, 175 | }, 176 | } 177 | 178 | zShapes = [][][]int{ 179 | { 180 | {0, 0, 0}, 181 | {1, 1, 0}, 182 | {0, 1, 1}, 183 | }, 184 | { 185 | {0, 1, 0}, 186 | {1, 1, 0}, 187 | {1, 0, 0}, 188 | }, 189 | { 190 | {1, 1, 0}, 191 | {0, 1, 1}, 192 | {0, 0, 0}, 193 | }, 194 | { 195 | {0, 0, 1}, 196 | {0, 1, 1}, 197 | {0, 1, 0}, 198 | }, 199 | } 200 | 201 | // Generate a tetromino randomly 202 | // Handle other tetrominos similarly 203 | randShapes = [][][][]int{ 204 | tShapes, 205 | iShapes, 206 | oShapes, 207 | lShapes, 208 | jShapes, 209 | sShapes, 210 | zShapes, 211 | } 212 | ) 213 | 214 | // Define T-shaped and other tetrominos including their rotation states in advance 215 | func (g *Game) createNewTetromino() Tetromino { 216 | next := g.next 217 | g.next = g.generateNewTetromino() 218 | return next 219 | } 220 | 221 | func (g *Game) generateNewTetromino() Tetromino { 222 | choice := randShapes[rand.Intn(len(randShapes))] 223 | 224 | // Randomly select a tetromino and return it 225 | return Tetromino{ 226 | shapes: choice, 227 | rotation: 0, 228 | x: 4, 229 | y: 0, 230 | } 231 | } 232 | 233 | // Initialization 234 | func NewGame() *Game { 235 | rand.Seed(time.Now().UnixNano()) 236 | game := &Game{} 237 | game.next = game.generateNewTetromino() 238 | game.tetromino = game.createNewTetromino() 239 | game.scene = "title" 240 | return game 241 | } 242 | 243 | // Game update process 244 | func (g *Game) Update() error { 245 | if g.scene != "game" { 246 | return nil 247 | } 248 | 249 | if time.Since(lastDropTime) > dropInterval { 250 | if g.isValidPosition(g.tetromino.x, g.tetromino.y+1, g.currentShape()) { 251 | g.tetromino.y++ 252 | } else { 253 | // Lock the block and generate a new one 254 | g.lockTetromino() 255 | g.tetromino = g.createNewTetromino() 256 | if !g.isValidPosition(g.tetromino.x, g.tetromino.y, g.currentShape()) { 257 | g.scene = "gameover" 258 | } 259 | } 260 | lastDropTime = time.Now() 261 | } 262 | 263 | if time.Since(lastMoveTime) > moveInterval { 264 | // Move left 265 | if koebiten.KeyPressDuration(koebiten.KeyUp) > 0 { 266 | if g.isValidPosition(g.tetromino.x-1, g.tetromino.y, g.currentShape()) { 267 | g.tetromino.x-- 268 | } 269 | if koebiten.KeyPressDuration(koebiten.KeyUp) < 10 { 270 | lastMoveTime = time.Now().Add(moveInterval * 2) 271 | } else { 272 | lastMoveTime = time.Now() 273 | } 274 | } 275 | // Move right 276 | if koebiten.KeyPressDuration(koebiten.KeyDown) > 0 { 277 | if g.isValidPosition(g.tetromino.x+1, g.tetromino.y, g.currentShape()) { 278 | g.tetromino.x++ 279 | } 280 | if koebiten.KeyPressDuration(koebiten.KeyDown) < 10 { 281 | lastMoveTime = time.Now().Add(moveInterval * 2) 282 | } else { 283 | lastMoveTime = time.Now() 284 | } 285 | } 286 | // Move down 287 | if koebiten.KeyPressDuration(koebiten.KeyLeft) > 0 { 288 | if g.isValidPosition(g.tetromino.x, g.tetromino.y+1, g.currentShape()) { 289 | g.tetromino.y++ 290 | } else { 291 | // Lock the block and generate a new one 292 | g.lockTetromino() 293 | g.tetromino = g.createNewTetromino() 294 | if !g.isValidPosition(g.tetromino.x, g.tetromino.y, g.currentShape()) { 295 | g.scene = "gameover" 296 | } 297 | } 298 | lastMoveTime = time.Now() 299 | } 300 | } 301 | 302 | // Move down faster 303 | if koebiten.IsKeyJustPressed(koebiten.KeyRight) || koebiten.IsKeyJustPressed(koebiten.Key9) { 304 | for g.isValidPosition(g.tetromino.x, g.tetromino.y+1, g.currentShape()) { 305 | g.tetromino.y++ 306 | } 307 | // Lock the block and generate a new one 308 | g.lockTetromino() 309 | g.tetromino = g.createNewTetromino() 310 | if !g.isValidPosition(g.tetromino.x, g.tetromino.y, g.currentShape()) { 311 | g.scene = "gameover" 312 | } 313 | } 314 | 315 | if koebiten.KeyPressDuration(koebiten.KeyUp) == 0 && 316 | koebiten.KeyPressDuration(koebiten.KeyDown) == 0 && 317 | koebiten.KeyPressDuration(koebiten.KeyLeft) == 0 && 318 | koebiten.KeyPressDuration(koebiten.KeyRight) == 0 { 319 | lastMoveTime = time.Time{} 320 | } 321 | 322 | // Rotate the block 323 | if koebiten.IsKeyJustPressed(koebiten.Key4) || koebiten.IsKeyJustPressed(koebiten.KeyRotaryLeft) || koebiten.IsKeyJustPressed(koebiten.Key1) { 324 | g.rotateTetromino(true) 325 | } else if koebiten.IsKeyJustPressed(koebiten.Key8) || koebiten.IsKeyJustPressed(koebiten.KeyRotaryRight) || koebiten.IsKeyJustPressed(koebiten.Key0) { 326 | g.rotateTetromino(false) 327 | } 328 | return nil 329 | } 330 | 331 | // Calculate how far the tetromino can fall 332 | func (g *Game) calculateDropPosition() int { 333 | ghostY := g.tetromino.y 334 | for g.isValidPosition(g.tetromino.x, ghostY+1, g.currentShape()) { 335 | ghostY++ 336 | } 337 | return ghostY 338 | } 339 | 340 | func (g *Game) isValidPosition(x, y int, shape [][]int) bool { 341 | for i, row := range shape { 342 | for j, cell := range row { 343 | if cell == 0 { 344 | continue 345 | } 346 | newX := x + j 347 | newY := y + i 348 | // Check if it goes out of the board range 349 | if newX < 0 || newX >= len(g.board) || newY >= len(g.board[0]) { 350 | return false 351 | } 352 | // Check if it collides with an existing block 353 | if g.board[newX][newY] == 1 { 354 | return false 355 | } 356 | } 357 | } 358 | return true 359 | } 360 | 361 | func (g *Game) lockTetromino() { 362 | for i, row := range g.currentShape() { 363 | for j, cell := range row { 364 | if cell == 1 { 365 | x := g.tetromino.x + j 366 | y := g.tetromino.y + i 367 | g.board[x][y] = 1 368 | } 369 | } 370 | } 371 | g.clearLines() 372 | } 373 | 374 | func (g *Game) clearLines() { 375 | cnt := 0 376 | for y := 0; y < len(g.board[0]); y++ { 377 | full := true 378 | for x := 0; x < len(g.board); x++ { 379 | if g.board[x][y] == 0 { 380 | full = false 381 | break 382 | } 383 | } 384 | if full { 385 | // Clear the line and move the upper blocks down 386 | for yy := y; yy > 0; yy-- { 387 | for x := 0; x < len(g.board); x++ { 388 | g.board[x][yy] = g.board[x][yy-1] 389 | } 390 | } 391 | // Clear the top row 392 | for x := 0; x < len(g.board); x++ { 393 | g.board[x][0] = 0 394 | } 395 | cnt++ 396 | } 397 | } 398 | 399 | switch cnt { 400 | case 1: 401 | g.score += 1 402 | case 2: 403 | g.score += 2 404 | case 3: 405 | g.score += 4 406 | case 4: 407 | g.score += 10 408 | } 409 | if cnt > 0 { 410 | if (g.score % 10) == 0 { 411 | dropInterval = dropInterval * 900 / 1000 412 | } 413 | } 414 | } 415 | 416 | // Tetromino rotation process 417 | func (g *Game) rotateTetromino(reverse bool) { 418 | // Calculate the next rotation state 419 | nextRotation := (g.tetromino.rotation + 1) % len(g.tetromino.shapes) 420 | if reverse { 421 | nextRotation = (g.tetromino.rotation + len(g.tetromino.shapes) - 1) % len(g.tetromino.shapes) 422 | } 423 | 424 | // Check if the rotated shape is valid 425 | if g.isValidPosition(g.tetromino.x, g.tetromino.y, g.tetromino.shapes[nextRotation]) { 426 | // If valid, apply the rotation 427 | g.tetromino.rotation = nextRotation 428 | } else { 429 | // If the rotated position is invalid, try wall kicks (move left or right and attempt to rotate) 430 | if g.isValidPosition(g.tetromino.x-1, g.tetromino.y, g.tetromino.shapes[nextRotation]) { 431 | // Move left and rotate 432 | g.tetromino.x-- 433 | g.tetromino.rotation = nextRotation 434 | } else if g.isValidPosition(g.tetromino.x+1, g.tetromino.y, g.tetromino.shapes[nextRotation]) { 435 | // Move right and rotate 436 | g.tetromino.x++ 437 | g.tetromino.rotation = nextRotation 438 | } 439 | } 440 | } 441 | 442 | // Function to get the current shape of the tetromino 443 | func (g *Game) currentShape() [][]int { 444 | return g.tetromino.shapes[g.tetromino.rotation] 445 | } 446 | 447 | func (g *Game) Draw(screen *koebiten.Image) { 448 | switch g.scene { 449 | case "title": 450 | g.drawTitle(screen) 451 | case "game": 452 | g.drawGame(screen) 453 | case "gameover": 454 | g.drawGameover(screen) 455 | } 456 | } 457 | 458 | func (g *Game) drawTitle(screen *koebiten.Image) { 459 | koebiten.Println("click") 460 | koebiten.Println("to start") 461 | koebiten.Println("") 462 | koebiten.Println("high score") 463 | koebiten.Println(" ", g.highScore) 464 | if isAnyKeyJustPressed() { 465 | g.score = 0 466 | dropInterval = time.Duration(1000 * time.Millisecond) 467 | lastDropTime = time.Now() 468 | moveInterval = time.Duration(100 * time.Millisecond) 469 | lastMoveTime = time.Now() 470 | lastMoveAction = time.Duration(0) 471 | for i := range g.board { 472 | for j := range g.board[i] { 473 | g.board[i][j] = 0 474 | } 475 | } 476 | g.next = g.generateNewTetromino() 477 | g.tetromino = g.createNewTetromino() 478 | g.scene = "game" 479 | } 480 | } 481 | 482 | // Game drawing process 483 | func (g *Game) drawGame(screen *koebiten.Image) { 484 | koebiten.DrawLine(screen, 1, 0, 1, gridSize*24, frameColor) 485 | koebiten.DrawLine(screen, gridSize*12+2, 0, gridSize*12+2, gridSize*24+1, frameColor) 486 | koebiten.DrawLine(screen, 1, gridSize*24+1, gridSize*12+2, gridSize*24+1, frameColor) 487 | 488 | // Calculate the falling position of the tetromino 489 | ghostY := g.calculateDropPosition() 490 | 491 | // Draw the guide 492 | for y, row := range g.currentShape() { 493 | for x, cell := range row { 494 | if cell == 1 { 495 | koebiten.DrawRect(screen, int((g.tetromino.x+x)*gridSize+1)+2, int((ghostY+y)*gridSize+1), int(gridSize-2), int(gridSize-2), frameColor) 496 | } 497 | } 498 | } 499 | 500 | // Draw the game board 501 | for y := 0; y < 24; y++ { 502 | for x := 0; x < 12; x++ { 503 | if g.board[x][y] == 1 { 504 | koebiten.DrawFilledRect(screen, int(x*gridSize)+2, int(y*gridSize), gridSize-1, gridSize-1, frameColor) 505 | } 506 | } 507 | } 508 | 509 | // Draw the tetromino 510 | for y, row := range g.currentShape() { 511 | for x, cell := range row { 512 | if cell == 1 { 513 | koebiten.DrawFilledRect(screen, int((g.tetromino.x+x)*gridSize)+2, int((g.tetromino.y+y)*gridSize), gridSize-1, gridSize-1, frameColor) 514 | } 515 | } 516 | } 517 | 518 | koebiten.DrawText(screen, "Score: "+strconv.Itoa(g.score), &tinyfont.Org01, 0, gridSize*24+6, frameColor) 519 | 520 | // Draw the next tetromino 521 | xoffset := 0 522 | if g.tetromino.x < 4 { 523 | xoffset = 8 524 | } 525 | for y, row := range g.next.shapes[0] { 526 | for x, cell := range row { 527 | if cell == 1 { 528 | koebiten.DrawRect(screen, int((x+xoffset)*gridSize+1)+2, int((y)*gridSize+1), int(gridSize-2), int(gridSize-2), frameColor) 529 | } 530 | } 531 | } 532 | } 533 | 534 | func (g *Game) drawGameover(screen *koebiten.Image) { 535 | koebiten.Println("gameover") 536 | koebiten.Println("score: ", g.score) 537 | if isAnyKeyJustPressed() { 538 | if g.highScore < g.score { 539 | g.highScore = g.score 540 | } 541 | g.scene = "title" 542 | } 543 | } 544 | 545 | // Screen size 546 | func (g *Game) Layout(outsideWidth, outsideHeight int) (w, h int) { 547 | return screenWidth, screenHeight 548 | } 549 | 550 | func isAnyKeyJustPressed() bool { 551 | return len(koebiten.AppendJustPressedKeys(nil)) > 0 552 | } 553 | --------------------------------------------------------------------------------