From 0245a5cb436e5a7d79054c157d704900cd0913e7 Mon Sep 17 00:00:00 2001 From: Henrik Hautakoski Date: Sun, 14 Sep 2025 08:38:30 +0200 Subject: [PATCH] Initial commit --- .gitignore | 1 + Makefile | 6 ++ README.md | 23 +++++++ assets/def.go | 55 ++++++++++++++++ assets/sprite.xcf | Bin 0 -> 36247 bytes assets/sprites.png | Bin 0 -> 1209 bytes engine/graphics/font/tilefont.go | 41 ++++++++++++ engine/graphics/texture.go | 9 +++ engine/graphics/tile.go | 18 ++++++ engine/render/context.go | 24 +++++++ engine/render/rect.go | 26 ++++++++ engine/render/render.go | 107 +++++++++++++++++++++++++++++++ engine/render/scale.go | 56 ++++++++++++++++ engine/render/text.go | 31 +++++++++ engine/render/texture.go | 8 +++ engine/system/hypr.go | 23 +++++++ game/draw/grid/grid.go | 47 ++++++++++++++ game/draw/renderer.go | 40 ++++++++++++ game/draw/theme.go | 20 ++++++ go.mod | 11 ++++ go.sum | 8 +++ main.go | 56 ++++++++++++++++ 22 files changed, 610 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 assets/def.go create mode 100644 assets/sprite.xcf create mode 100644 assets/sprites.png create mode 100644 engine/graphics/font/tilefont.go create mode 100644 engine/graphics/texture.go create mode 100644 engine/graphics/tile.go create mode 100644 engine/render/context.go create mode 100644 engine/render/rect.go create mode 100644 engine/render/render.go create mode 100644 engine/render/scale.go create mode 100644 engine/render/text.go create mode 100644 engine/render/texture.go create mode 100644 engine/system/hypr.go create mode 100644 game/draw/grid/grid.go create mode 100644 game/draw/renderer.go create mode 100644 game/draw/theme.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1dce0e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/tetris diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7017b46 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +GO = go + +.PHONY: tetris + +tetris : + $(GO) build -o $@ diff --git a/README.md b/README.md new file mode 100644 index 0000000..b56fa7b --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Tetris in golang + +## Roadmap + +- [x] Initial Commit - Project Scaffolding (rendering engine) +- [ ] 0.1 - Grid +- [ ] 0.2 - Basic shapes (falling + collision with floor) +- [ ] 0.3 - Shapes: Lock + spawn +- [ ] 0.4 - Collision with with blocks in grid +- [ ] 0.5 - left/right movement +- [ ] 0.6 - Shapes rotation +- [ ] 0.7 - Soft Drop +- [ ] 0.8 - Clear lines +- [ ] 0.9 - Score +- [ ] 0.10 - Next shape +- [ ] 0.11 - RNG Shape generation +- [ ] 0.12 - Sound +- [ ] 0.13 - State machine: Menu, Gameplay and Game Over screen. +- [ ] 1.0 - Bugfixes + +## Future things (aka Rad stuffs!) + +- [ ] Animations (Clear lines) diff --git a/assets/def.go b/assets/def.go new file mode 100644 index 0000000..4524c96 --- /dev/null +++ b/assets/def.go @@ -0,0 +1,55 @@ +package assets + +import ( + _ "embed" + + "tetris/engine/graphics/font" +) + +//go:embed sprites.png +var Sprite []byte + +var Font = font.TileFont{ + Charmap: map[rune]byte{ + 'a': 0x20, + 'b': 0x21, + 'c': 0x22, + 'd': 0x23, + 'e': 0x24, + 'f': 0x25, + 'g': 0x26, + 'h': 0x27, + 'i': 0x28, + 'j': 0x29, + 'k': 0x2a, + 'l': 0x2b, + 'm': 0x2c, + 'n': 0x30, + 'o': 0x31, + 'p': 0x32, + 'q': 0x33, + 'r': 0x34, + 's': 0x35, + 't': 0x36, + 'u': 0x37, + 'v': 0x38, + 'w': 0x39, + 'x': 0x3a, + 'y': 0x3b, + 'z': 0x3c, + '0': 0x40, + '1': 0x41, + '2': 0x42, + '3': 0x43, + '4': 0x44, + '5': 0x45, + '6': 0x46, + '7': 0x47, + '8': 0x48, + '9': 0x49, + '.': 0x4a, + '-': 0x4b, + '"': 0x4c, + }, + CaseInsensitive: true, +} diff --git a/assets/sprite.xcf b/assets/sprite.xcf new file mode 100644 index 0000000000000000000000000000000000000000..982b2722e2e337b9553bd2050031d340fcaadbd5 GIT binary patch literal 36247 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 'A' && char <= 'Z' { + char = char + 0x20 + } + + if offset, found := f.Charmap[char]; found { + return &graphics.Tile{ + Size: 8, + X: offset & 0xF, + Y: offset >> 4, + } + } + return nil +} diff --git a/engine/graphics/texture.go b/engine/graphics/texture.go new file mode 100644 index 0000000..6497fc7 --- /dev/null +++ b/engine/graphics/texture.go @@ -0,0 +1,9 @@ +package graphics + +import rl "github.com/gen2brain/raylib-go/raylib" + +func LoadTextureFromMemory(fileType string, data []byte) rl.Texture2D { + img := rl.LoadImageFromMemory(fileType, data, int32(len(data))) + defer rl.UnloadImage(img) + return rl.LoadTextureFromImage(img) +} diff --git a/engine/graphics/tile.go b/engine/graphics/tile.go new file mode 100644 index 0000000..de37812 --- /dev/null +++ b/engine/graphics/tile.go @@ -0,0 +1,18 @@ +package graphics + +import rl "github.com/gen2brain/raylib-go/raylib" + +type Tile struct { + Size byte + X, Y byte +} + +// GetTexRect returns the texture rectangle. +func (t Tile) GetTexRect() rl.Rectangle { + return rl.Rectangle{ + X: float32(t.X * t.Size), + Y: float32(t.Y * t.Size), + Width: float32(t.Size), + Height: float32(t.Size), + } +} diff --git a/engine/render/context.go b/engine/render/context.go new file mode 100644 index 0000000..540afad --- /dev/null +++ b/engine/render/context.go @@ -0,0 +1,24 @@ +package render + +import ( + gfxfont "tetris/engine/graphics/font" + + rl "github.com/gen2brain/raylib-go/raylib" +) + +var ( + texture rl.Texture2D + font *gfxfont.TileFont = nil +) + +func SetTexture(tex rl.Texture2D) { + texture = tex +} + +func GetTexture() rl.Texture2D { + return texture +} + +func SetFont(fnt *gfxfont.TileFont) { + font = fnt +} diff --git a/engine/render/rect.go b/engine/render/rect.go new file mode 100644 index 0000000..3ad993b --- /dev/null +++ b/engine/render/rect.go @@ -0,0 +1,26 @@ +package render + +import ( + "image/color" + + rl "github.com/gen2brain/raylib-go/raylib" +) + +// DrawRectBorder draw a rectangle with a border around it. +// Note that the rectangle passed as the 'rect' parameter to the function represents the actual +// rectangle. The border will be drawn around it in all directions (outer border). +func DrawRectBorder(rect rl.RectangleInt32, col color.RGBA, border_size int32, border_col color.RGBA) { + rl.DrawRectangleRec(rl.Rectangle{ + X: float32(rect.X), + Y: float32(rect.Y), + Width: float32(rect.Width), + Height: float32(rect.Height), + }, col) + + rl.DrawRectangleLinesEx(rl.Rectangle{ + X: float32(rect.X - border_size), + Y: float32(rect.Y - border_size), + Width: float32(rect.Width + (border_size * 2)), + Height: float32(rect.Height + (border_size * 2)), + }, float32(border_size), border_col) +} diff --git a/engine/render/render.go b/engine/render/render.go new file mode 100644 index 0000000..f72e9c5 --- /dev/null +++ b/engine/render/render.go @@ -0,0 +1,107 @@ +package render + +import ( + "image/color" + + "tetris/engine/system" + + rl "github.com/gen2brain/raylib-go/raylib" +) + +type Config struct { + Title string + + // Window + WindowWidth int32 + WindowHeight int32 + WindowFlags uint32 + + // Render target resolution + RenderHeight int32 + RenderWidth int32 + + ScaleFlags ScaleFlags +} + +// target is the off-screen render target where all drawing operations are performed. +// It has a fixed resolution and is later scaled to the window size. +var target rl.RenderTexture2D + +var scaleFlags ScaleFlags + +// Init initializes the rendering system. +// +// It creates an off-screen render texture with the fixed resolution defined by config.RenderWidth/Height. +// This texture is where all tiles and game graphics are drawn before being scaled and presented. +func Init(config Config) { + // hyprland has a bug where the height of the window is 37 pixels taller than requested. + // so we just subtract it here. + if system.IsHyprland() { + config.WindowHeight = config.WindowHeight - 37 + } + + scaleFlags = config.ScaleFlags + + if config.WindowFlags > 0 { + rl.SetConfigFlags(config.WindowFlags) + } + + // Setup window + rl.InitWindow(config.WindowWidth, config.WindowHeight, config.Title) + + // Initialize render texture. + target = rl.LoadRenderTexture(config.RenderWidth, config.RenderHeight) + rl.SetTextureFilter(target.Texture, rl.TextureFilterNearest) +} + +// Exit shuts down the rendering system. +// +// It unloads the render texture and frees any associated GPU resources. +// This should be called before the application exits. +func Exit() { + rl.UnloadRenderTexture(target) + rl.CloseWindow() +} + +// Begin prepares the rendering system for a new frame. +// +// It binds the off-screen texture target and clears it to a black background. +// All tile drawing operations (e.g., DrawTile) should occur after Begin() and +// before End(). +func Begin(col color.RGBA) { + // Bind texture so that all draw calls after are applied to target texture + rl.BeginTextureMode(target) + // Clear the texture + rl.ClearBackground(col) +} + +// End finalizes the current frame and displays it on the screen. +// +// It ends the off-screen drawing session and stretches the texture target +// to fill the window. This function must be called after all drawing is done. +func End() { + // End drawing to the texture target. + rl.EndTextureMode() + + // Begin drawing to screen buffer. + rl.BeginDrawing() + + // Define source rectangle (flip vertically to match coordinate systems). + src := rl.Rectangle{ + X: 0, + Y: 0, + Width: float32(target.Texture.Width), + Height: -float32(target.Texture.Height), + } + + // Calculate the destination rectangle (window). + dest := scale(target.Texture.Width, target.Texture.Height, + int32(rl.GetRenderWidth()), int32(rl.GetRenderHeight()), + scaleFlags) + + // Blit the off-screen texture to the screen's back buffer. + rl.DrawTexturePro(target.Texture, src, dest, rl.Vector2Zero(), 0.0, rl.White) + + // Swap buffers + rl.EndDrawing() +} diff --git a/engine/render/scale.go b/engine/render/scale.go new file mode 100644 index 0000000..5b179b0 --- /dev/null +++ b/engine/render/scale.go @@ -0,0 +1,56 @@ +package render + +import ( + "math" + + rl "github.com/gen2brain/raylib-go/raylib" +) + +type ScaleFlags byte + +const ( + // Use integer scaling (pixel-perfect). Implies SCALE_NO_DOWNSCALE because integer < 1 becomes 0. + SCALE_INTEGER ScaleFlags = 1 << iota + + // Do not allow shrinking below 1 (clamp scale >= 1). + SCALE_NO_DOWNSCALE + + // Use "cover" (crop) instead of "contain" (letterbox). Default is contain. + SCALE_COVER +) + +func scale(srcW, srcH, destW, destH int32, flags ScaleFlags) rl.Rectangle { + scale := float64(1) + sx := float64(destW) / float64(srcW) + sy := float64(destH) / float64(srcH) + + // Contain (letterbox) uses min; Cover (crop) uses max. + if flags&SCALE_COVER != 0 { + scale = math.Max(sx, sy) + } else { + scale = math.Min(sx, sy) + } + + if flags&SCALE_INTEGER != 0 { + // For contain, floor; for cover, ceil. + if flags&SCALE_COVER != 0 { + scale = math.Ceil(scale) + } else { + scale = math.Floor(scale) + } + // IntegerScale implies no fractional; clamp at 1 to avoid zero. + scale = max(1.0, scale) + } else if flags&SCALE_NO_DOWNSCALE != 0 && scale < 1.0 { + scale = 1.0 + } + + w := float64(srcW) * scale + h := float64(srcH) * scale + + return rl.Rectangle{ + X: float32(math.Floor((float64(destW) - w) / 2.0)), + Y: float32(math.Floor((float64(destH) - h) / 2.0)), + Width: float32(w), + Height: float32(h), + } +} diff --git a/engine/render/text.go b/engine/render/text.go new file mode 100644 index 0000000..471f85e --- /dev/null +++ b/engine/render/text.go @@ -0,0 +1,31 @@ +package render + +import ( + "image/color" + + rl "github.com/gen2brain/raylib-go/raylib" +) + +func DrawText(x, y, size int32, text string, col color.RGBA) { + destRect := rl.Rectangle{ + X: float32(x), + Y: float32(y), + Width: float32(size), + Height: float32(size), + } + + for _, ch := range text { + if ch == ' ' { + destRect.X += float32(size) + continue + } + + tile := font.GetTile(ch) + if tile == nil { + destRect.X += float32(size) + continue + } + rl.DrawTexturePro(texture, tile.GetTexRect(), destRect, rl.Vector2Zero(), 0, col) + destRect.X += float32(size) + } +} diff --git a/engine/render/texture.go b/engine/render/texture.go new file mode 100644 index 0000000..9fee159 --- /dev/null +++ b/engine/render/texture.go @@ -0,0 +1,8 @@ +package render + +import rl "github.com/gen2brain/raylib-go/raylib" + +// DrawTextureRec - Draw a rectangle from the current set texture. +func DrawTextureRec(src rl.Rectangle, dest rl.Rectangle) { + rl.DrawTexturePro(texture, src, dest, rl.Vector2Zero(), 0.0, rl.White) +} diff --git a/engine/system/hypr.go b/engine/system/hypr.go new file mode 100644 index 0000000..8483e06 --- /dev/null +++ b/engine/system/hypr.go @@ -0,0 +1,23 @@ +package system + +import ( + "os" + "path/filepath" + "strings" +) + +func IsHyprland() bool { + // Primary signals + xdg := os.Getenv("XDG_RUNTIME_DIR") + his := os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") + if xdg != "" && his != "" { + sock := filepath.Join(xdg, "hypr", his, ".socket.sock") + if st, err := os.Stat(sock); err == nil && !st.IsDir() { + return true + } + } + + // Fallback: desktop hint + hint := strings.ToLower(os.Getenv("XDG_CURRENT_DESKTOP")) + return strings.Contains(hint, "hypr") +} diff --git a/game/draw/grid/grid.go b/game/draw/grid/grid.go new file mode 100644 index 0000000..5bb25a6 --- /dev/null +++ b/game/draw/grid/grid.go @@ -0,0 +1,47 @@ +package grid + +import ( + "image/color" + + "tetris/engine/render" + + rl "github.com/gen2brain/raylib-go/raylib" +) + +const ( + // How many pixels wide and tall each cell is. + CELL_SIZE = 32 + + // Number of pixels between each cell + CELL_SPACING = 1 +) + +func DrawBackground(rect rl.RectangleInt32, col color.RGBA, border_size int32, border_col color.RGBA) { + render.DrawRectBorder(rl.RectangleInt32{ + X: int32(rect.X), + Y: int32(rect.Y), + Width: int32((rect.Width * (CELL_SIZE + CELL_SPACING)) + CELL_SPACING), + Height: int32((rect.Height * (CELL_SIZE + CELL_SPACING)) + CELL_SPACING), + }, col, border_size, border_col) +} + +func Draw(rect rl.RectangleInt32) { + // offset for background. + rect.X = rect.X + CELL_SPACING + rect.Y = rect.Y + CELL_SPACING + + cell := rl.Rectangle{ + X: 0, + Y: 0, + Width: float32(CELL_SIZE), + Height: float32(CELL_SIZE), + } + + for y := range rect.Height { + for x := range rect.Width { + cell.X = float32(rect.X + (x * (CELL_SIZE + CELL_SPACING))) + cell.Y = float32(rect.Y + (y * (CELL_SIZE + CELL_SPACING))) + rl.DrawRectangleRec(cell, rl.Black) + } + } +} diff --git a/game/draw/renderer.go b/game/draw/renderer.go new file mode 100644 index 0000000..b94a427 --- /dev/null +++ b/game/draw/renderer.go @@ -0,0 +1,40 @@ +package draw + +import ( + "tetris/engine/render" + "tetris/game/draw/grid" + + rl "github.com/gen2brain/raylib-go/raylib" +) + +const ( + // Border width when drawing frames + BORDER_WIDTH = 8 + + // Size for normal text + TEXT_SIZE = 32 + + // Text size for header texts + HEADER_TEXT_SIZE = 16 +) + +type Renderer struct { + Theme *Theme +} + +func (r Renderer) DrawText(x int32, y int32, text string) { + render.DrawText(x, y, TEXT_SIZE, text, r.Theme.Text) +} + +func (r Renderer) DrawHeaderText(x int32, y int32, text string) { + render.DrawText(x, y, HEADER_TEXT_SIZE, text, r.Theme.TextHeader) +} + +func (r Renderer) DrawFrame(rect rl.RectangleInt32) { + render.DrawRectBorder(rect, r.Theme.FrameBG, BORDER_WIDTH, r.Theme.FrameBorder) +} + +func (r Renderer) DrawGrid(rect rl.RectangleInt32) { + grid.DrawBackground(rect, r.Theme.GridBackground, BORDER_WIDTH, r.Theme.FrameBorder) + grid.Draw(rect) +} diff --git a/game/draw/theme.go b/game/draw/theme.go new file mode 100644 index 0000000..bc37307 --- /dev/null +++ b/game/draw/theme.go @@ -0,0 +1,20 @@ +package draw + +import ( + "image/color" +) + +// Theme holds the different colors used when rendering. +type Theme struct { + // Frame colors + FrameBG color.RGBA + FrameBorder color.RGBA + + // Text colors + TextHeader color.RGBA + Text color.RGBA + + // Grid Colors + GridBackground color.RGBA + GridEmptyCell color.RGBA +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..00b95c8 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module tetris + +go 1.24.6 + +require github.com/gen2brain/raylib-go/raylib v0.55.1 + +require ( + github.com/ebitengine/purego v0.7.1 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect + golang.org/x/sys v0.20.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aa78a1e --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA= +github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= +github.com/gen2brain/raylib-go/raylib v0.55.1 h1:1rdc10WvvYjtj7qijHnV9T38/WuvlT6IIL+PaZ6cNA8= +github.com/gen2brain/raylib-go/raylib v0.55.1/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..243d8fd --- /dev/null +++ b/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "image/color" + + "tetris/assets" + "tetris/engine/graphics" + "tetris/engine/render" + "tetris/game/draw" + + rl "github.com/gen2brain/raylib-go/raylib" +) + +func main() { + render.Init(render.Config{ + Title: "Tetris", + WindowWidth: 685, + WindowHeight: 600, + RenderWidth: 685, + RenderHeight: 600, + ScaleFlags: render.SCALE_INTEGER, + }) + defer render.Exit() + + // Load texture + texture := graphics.LoadTextureFromMemory(".png", assets.Sprite) + defer rl.UnloadTexture(texture) + render.SetTexture(texture) + render.SetFont(&assets.Font) + + r := draw.Renderer{ + Theme: &draw.Theme{ + FrameBG: color.RGBA{R: 30, G: 30, B: 46, A: 255}, + FrameBorder: color.RGBA{R: 242, G: 205, B: 205, A: 255}, + TextHeader: color.RGBA{R: 242, G: 205, B: 205, A: 255}, + Text: color.RGBA{R: 205, G: 214, B: 244, A: 255}, + GridBackground: color.RGBA{R: 17, G: 17, B: 27, A: 255}, + }, + } + + for !rl.WindowShouldClose() { + render.Begin(r.Theme.GridBackground) + r.DrawGrid(rl.RectangleInt32{ + X: 25, + Y: 25, + Width: 10, + Height: 16, + }) + r.DrawFrame(rl.RectangleInt32{X: 400, Y: 25, Width: 250, Height: 100}) + r.DrawHeaderText(410, 30, "Score") + r.DrawText(410, 65, "999999") + r.DrawFrame(rl.RectangleInt32{X: 400, Y: 150, Width: 250, Height: 200}) + + render.End() + } +}