Merge pull request #2 from iliadenisov/refactor-and-add-docs
Refactor and add description
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Ilia Denisov
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,5 +1,44 @@
|
|||||||
# 15 Puzzle game with Go
|
# 15 Puzzle game with Go
|
||||||
|
|
||||||
|
🚀 [Play in Telegram](https://t.me/puzzle_15_bot?startapp).
|
||||||
|
Game has [simple rules](https://en.wikipedia.org/wiki/15_puzzle).
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The intentions of the project is to create a simple proof-of-concept demonstrating that a [Telegram Mini App](https://core.telegram.org/bots/webapps) can be developed with Go and [WebAssembly](https://go.dev/wiki/WebAssembly).
|
||||||
|
|
||||||
|
✨ Graphics are made with [Ebitengine](https://github.com/hajimehoshi/ebiten).
|
||||||
|
|
||||||
|
Basic "features" include: a splash screen, game move sound, a switchable silent mode without moves count,
|
||||||
|
a players' rating table, a congratulations screen for achieving 1st place,
|
||||||
|
a pin-code protected game statistics screen,
|
||||||
|
and backend API requests secured through [data validation](https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app).
|
||||||
|
|
||||||
|
## Running game
|
||||||
|
|
||||||
|
Local gameplay can be initiated using the following command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go run ./cmd/ui
|
||||||
|
```
|
||||||
|
|
||||||
|
A fully functional Mini App requires a web server.
|
||||||
|
You can use [`Dockerfile`](Dockerfile) to build a Docker Image
|
||||||
|
containing all necessary artifacts: the server binary, the WebAssemply (wasm) binary, and html page.
|
||||||
|
Certain environment variables should be set for the server to start:
|
||||||
|
|
||||||
|
| Env | Description |
|
||||||
|
| - | - |
|
||||||
|
| **`BOT_TOKEN`** | Telegram Bot [API Token](https://core.telegram.org/bots/tutorial). |
|
||||||
|
| **`DATA_FILE`** | Path to the file where games data will be stored. |
|
||||||
|
| **`ACCESS_CODE`** | Pin-code to access the Statistics Screen. |
|
||||||
|
| **`PROJECT_LINK`** | URL to the project's source code. |
|
||||||
|
| `SERVER_PORT` | Port for the server to listen for API requests, defaulting to `8080` if not set. |
|
||||||
|
| `CONTEXT_ROOT` | Server requests URI root path, defaulting to `/` if not set. |
|
||||||
|
| `STATIC_DIR` | Directory where static files are located, defaulting to the current directory if not set. |
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- [Gopher by gheimifurt](https://www.reddit.com/r/golang/comments/xdxb9a/gopher_ascii_art_for_bashrc/).
|
- [Ebitengine](https://github.com/hajimehoshi/ebiten) game engine by Hajime Hoshi.
|
||||||
|
- Font by [VileR](http://int10h.org) and [fly_indiz](http://old-dos.ru/index.php?page=files&mode=files&do=show&id=102798).
|
||||||
|
- ASCII Gopher by [gheimifurt](https://www.reddit.com/r/golang/comments/xdxb9a/gopher_ascii_art_for_bashrc/).
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
Credits:
|
Credits:
|
||||||
|
|
||||||
- [VileR](http://int10h.org/).
|
- [VileR](http://int10h.org).
|
||||||
- [fly_indiz](http://old-dos.ru/index.php?page=files&mode=files&do=show&id=102798).
|
- [fly_indiz](http://old-dos.ru/index.php?page=files&mode=files&do=show&id=102798).
|
||||||
|
|||||||
@@ -139,11 +139,11 @@ func Init(init ...func(*Controller)) error {
|
|||||||
init[i](c)
|
init[i](c)
|
||||||
}
|
}
|
||||||
c.screens[screenGame] = newGame(c.OnGameStart, c.OnGameSolve, c.UserStatsRequest)
|
c.screens[screenGame] = newGame(c.OnGameStart, c.OnGameSolve, c.UserStatsRequest)
|
||||||
c.screens[screenForm] = newForm(c.MonitoringRequest)
|
c.screens[screenForm] = newStats(c.MonitoringRequest)
|
||||||
c.screens[screenSplash] = newSplash(c.UrlOpener)
|
c.screens[screenSplash] = newSplash(c.UrlOpener)
|
||||||
c.screens[screenDebug], c.debugFn = newDebugOverlay()
|
c.screens[screenDebug], c.debugFn = newDebugOverlay()
|
||||||
|
|
||||||
c.SetLangCode(langCodeRu)
|
c.SetLangCode(langCodeEn)
|
||||||
|
|
||||||
return ebiten.RunGame(c)
|
return ebiten.RunGame(c)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const (
|
|||||||
codeInputTemplate = `[%s] [%s] [%s] [%s]`
|
codeInputTemplate = `[%s] [%s] [%s] [%s]`
|
||||||
)
|
)
|
||||||
|
|
||||||
type form struct {
|
type stats struct {
|
||||||
dials [dials]*button
|
dials [dials]*button
|
||||||
request func(string)
|
request func(string)
|
||||||
requestSent bool
|
requestSent bool
|
||||||
@@ -34,10 +34,10 @@ func codeBox(b byte) string {
|
|||||||
return "_"
|
return "_"
|
||||||
}
|
}
|
||||||
|
|
||||||
func newForm(request func(string)) *form {
|
func newStats(request func(string)) *stats {
|
||||||
col := func(i int) int { return (i%3)*puzzleTileSymW + 5 }
|
col := func(i int) int { return (i%3)*puzzleTileSymW + 5 }
|
||||||
row := func(i int) int { return (i/3)*puzzleTileSymH + 3 }
|
row := func(i int) int { return (i/3)*puzzleTileSymH + 3 }
|
||||||
f := &form{request: request}
|
f := &stats{request: request}
|
||||||
for i := 1; i < dials; i++ {
|
for i := 1; i < dials; i++ {
|
||||||
f.dials[i] = NewButton(stringFn(strconv.Itoa(i)), intFn(col(i-1)), intFn(row(i-1)))
|
f.dials[i] = NewButton(stringFn(strconv.Itoa(i)), intFn(col(i-1)), intFn(row(i-1)))
|
||||||
}
|
}
|
||||||
@@ -45,20 +45,20 @@ func newForm(request func(string)) *form {
|
|||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *form) ApiMonitoringHandler(m model.Monitoring) {
|
func (st *stats) ApiMonitoringHandler(m model.Monitoring) {
|
||||||
f.mon.Store(m)
|
st.mon.Store(m)
|
||||||
f.authFail = false
|
st.authFail = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *form) Tick(t ticker) {
|
func (st *stats) Tick(t ticker) {
|
||||||
if t == ticker1Hz && f.requestSent {
|
if t == ticker1Hz && st.requestSent {
|
||||||
f.authFail = f.mon.Load() == nil
|
st.authFail = st.mon.Load() == nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *form) Draw(s Screen) {
|
func (st *stats) Draw(s Screen) {
|
||||||
drawGameField(s)
|
drawGameField(s)
|
||||||
if v := f.mon.Load(); v != nil {
|
if v := st.mon.Load(); v != nil {
|
||||||
m := v.(model.Monitoring)
|
m := v.(model.Monitoring)
|
||||||
printHeader(s, "Usage Statistics", 0)
|
printHeader(s, "Usage Statistics", 0)
|
||||||
s.Print(fmt.Sprintf("Players: %d\nGames: %d\nSolved: %d", m.Users, m.GamesStarted, m.GamesSolved),
|
s.Print(fmt.Sprintf("Players: %d\nGames: %d\nSolved: %d", m.Users, m.GamesStarted, m.GamesSolved),
|
||||||
@@ -66,30 +66,30 @@ func (f *form) Draw(s Screen) {
|
|||||||
color.RGBA{0, 0xFF, 0xFF, 0xFF})
|
color.RGBA{0, 0xFF, 0xFF, 0xFF})
|
||||||
} else {
|
} else {
|
||||||
r := image.Rect(8, 1, len(codeInputTemplate)+4, 2)
|
r := image.Rect(8, 1, len(codeInputTemplate)+4, 2)
|
||||||
if f.authFail {
|
if st.authFail {
|
||||||
s.Fill(r, color.RGBA{0xFF, 0, 0, 0xFF})
|
s.Fill(r, color.RGBA{0xFF, 0, 0, 0xFF})
|
||||||
} else if f.idx == len(f.input) {
|
} else if st.idx == len(st.input) {
|
||||||
s.Fill(r, color.RGBA{0x80, 0x80, 0x80, 0xFF})
|
s.Fill(r, color.RGBA{0x80, 0x80, 0x80, 0xFF})
|
||||||
}
|
}
|
||||||
printHeader(s, fmt.Sprintf(codeInputTemplate, codeBox(f.input[0]), codeBox(f.input[1]), codeBox(f.input[2]), codeBox(f.input[3])), 0)
|
printHeader(s, fmt.Sprintf(codeInputTemplate, codeBox(st.input[0]), codeBox(st.input[1]), codeBox(st.input[2]), codeBox(st.input[3])), 0)
|
||||||
for i := range f.dials {
|
for i := range st.dials {
|
||||||
f.dials[i].Draw(s)
|
st.dials[i].Draw(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *form) Interact(a Audio, col, row int, t time.Duration) actionResult {
|
func (st *stats) Interact(a Audio, col, row int, t time.Duration) actionResult {
|
||||||
if (image.Point{col, row}).In(image.Rect(2, 1, puzzleSymX-2, 2)) {
|
if (image.Point{col, row}).In(image.Rect(2, 1, puzzleSymX-2, 2)) {
|
||||||
f.input = [4]byte{}
|
st.input = [4]byte{}
|
||||||
f.authFail = false
|
st.authFail = false
|
||||||
f.requestSent = false
|
st.requestSent = false
|
||||||
f.idx = 0
|
st.idx = 0
|
||||||
return resultSwitchGame
|
return resultSwitchGame
|
||||||
}
|
}
|
||||||
if f.mon.Load() == nil {
|
if st.mon.Load() == nil {
|
||||||
for i := range f.dials {
|
for i := range st.dials {
|
||||||
if f.dials[i].Interact(col, row) {
|
if st.dials[i].Interact(col, row) {
|
||||||
f.pressNextButton('0' + byte(i))
|
st.pressNextButton('0' + byte(i))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,17 +97,17 @@ func (f *form) Interact(a Audio, col, row int, t time.Duration) actionResult {
|
|||||||
return resultNone
|
return resultNone
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *form) pressNextButton(digit byte) {
|
func (st *stats) pressNextButton(digit byte) {
|
||||||
if f.idx >= len(f.input) {
|
if st.idx >= len(st.input) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f.input[f.idx] = digit
|
st.input[st.idx] = digit
|
||||||
f.idx++
|
st.idx++
|
||||||
if f.idx == len(f.input) {
|
if st.idx == len(st.input) {
|
||||||
f.request(string(f.input[:]))
|
st.request(string(st.input[:]))
|
||||||
f.requestSent = true
|
st.requestSent = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *form) Activate() {}
|
func (st *stats) Activate() {}
|
||||||
func (f *form) SetLang(langCode) {}
|
func (st *stats) SetLang(langCode) {}
|
||||||
@@ -2,7 +2,6 @@ package tgbot
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"github.com/go-telegram/bot"
|
"github.com/go-telegram/bot"
|
||||||
@@ -38,8 +37,4 @@ func (b *tgBot) Start() {
|
|||||||
go b.b.Start(b.ctx)
|
go b.b.Start(b.ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *tgBot) updateHandler(ctx context.Context, _ *bot.Bot, update *models.Update) {
|
func (b *tgBot) updateHandler(ctx context.Context, _ *bot.Bot, _ *models.Update) {}
|
||||||
if update != nil {
|
|
||||||
b.log.Info(fmt.Sprintf("update:\n%#v\n", *update))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user