Skip to main content
← Back to Blog

Building Bauhaus Echo: A Cross-Platform Puzzle Game from Scratch

How we built a Bauhaus-inspired memory puzzle with libGDX, shipped it to five platforms, and added global leaderboards with HMAC-signed score submissions

Bauhaus Echo is now available on the App Store, Google Play, itch.io, and the web. It is a visual memory game where you memorize tile arrangements, watch them shuffle, and reconstruct the original pattern.

The Concept

The game shows you a grid of colorful geometric tiles inspired by Bauhaus design. You get a few seconds to memorize their positions. Then the tiles spin, shrink, and scatter to the bottom of the screen. Your job is to drag them back to where they belong.

It started as a simple prototype: a 3x3 grid, a shuffle, and a timer. But the Bauhaus aesthetic gave the project a visual identity early on, and from there it grew into five distinct game modes with difficulty scaling, global leaderboards, and a full cross-platform release.

The Tiles: Bauhaus Blocks by art101

The tile collection features over 8,000 geometric designs from the Bauhaus Blocks collection by art101, with 500 hand-curated core tiles bundled in the app and additional DLC palette packs available for download. Each tile is a 256x256 pixel sprite drawn in the Bauhaus tradition: bold primary shapes, clean geometry, and striking color contrasts. The tiles are packed into a libGDX TextureAtlas spread across PNG sprite sheets for efficient GPU batching.

We chose art101's Bauhaus Blocks because they capture the movement's aesthetic perfectly — circles, triangles, and rectangles in red, blue, yellow, black, and white — while providing enough variety that the game rarely repeats tiles in a session. DLC packs introduce new palettes and styles — darkly, pop, pastel, and randomized variants — unlocked automatically with the ad-free purchase.

Each tile in-game has beveled 3D edges and the game board is styled like a wood-and-felt craft table, giving the abstract designs a physical, tactile quality.

Five Game Modes

  • Classic: the core experience. Memorize the full grid, watch the tiles scatter, then reconstruct the pattern before time runs out.
  • Speed: tiles appear one at a time in their correct position for a shrinking window of time, then drop to the tray. Place each before the next one arrives. The display time decays exponentially — starting at 28% of the Classic base time and shrinking with a 0.40 decay factor per tile.
  • Endless: Speed mode that never ends. Placed tiles have a lifespan of three more spawns, after which they fade out and rejoin the queue. Your score is total correct placements before you run out of lives.
  • Memory: traditional card-matching. All tiles face down. Flip two to find pairs. Matched pairs spin away, mismatches cost a life.
  • Zen: no timer, no score, no pressure. A free-form sandbox where you compose with tiles at your own pace. Filter tiles by palette, format, or collection, and browse the full catalog including DLC packs.

Each mode supports 2x2 through 5x5 grids and three difficulty levels (Easy at 1.7x time, Medium at 1.35x, Hard at 1.0x). Lives scale with grid size and difficulty so larger grids give more room for error.

How We Built It

The Stack

Bauhaus Echo is built with libGDX, a Java-based cross-platform game framework. A single shared codebase in the core module runs on all platforms, with thin launcher modules for each target:

  • Android via the standard Android SDK (compileSdk 35, minSdk 21)
  • iOS and iPad via RoboVM/MobiVM — an ahead-of-time compiler that translates Java bytecode to native ARM
  • Desktop via LWJGL3, packaged with Construo and bundled JRE via jlink for a self-contained native app
  • Web via GWT (Google Web Toolkit), which compiles the Java source to JavaScript

This means we write game logic once and it runs everywhere. Platform-specific code (ads, billing, leaderboard networking) is abstracted behind interfaces that each launcher implements.

Development Timeline

The project started with a Scene2D UI menu and basic tile grid rendering. From there, development followed a roughly additive pattern:

  1. Core gameplay: grid rendering, tile dragging, snap-to-grid placement, and the show-shuffle-reconstruct loop for Classic mode
  2. Configuration and polish: a config screen for grid size and difficulty, custom fonts, tile selection tooltips, and extended memorization timers
  3. New animations: tile spin, shrink, and slide animations using libGDX's Action system — parallel and sequential action compositions for smooth transitions
  4. Speed Mode: the conveyor-belt mechanic where tiles arrive one at a time with decaying display windows
  5. Endless Mode: building on Speed with tile cycling, lifespan tracking, and an in-game settings overlay for pausing mid-run
  6. Memory/Pairs Mode: face-down card-matching with flip animations and pair detection
  7. Audio: a music system with randomized track selection from a pool of five game tracks, plus sound effects for pickup, drop, rejection, and UI interactions
  8. Statistics and scores: per-mode, per-grid tracking of wins, losses, and best times, displayed in an expandable stats UI
  9. Monetization: AdMob banner ads on mobile with a one-time "Remove Ads" in-app purchase, and a paid desktop build on itch.io
  10. Cross-platform fixes: GWT build compatibility, iOS RoboVM quirks, Android 48-second startup time reduction (pre-building the UI texture atlas), and desktop jlink module resolution

One of the more satisfying fixes was smart tile snapping. Early on, dragging a tile near an occupied cell would try to place it there, causing overlap. The fix was straightforward: Level.getClosestOpenTargetIndex() finds the nearest unoccupied cell instead of just the nearest cell, so tiles always snap to valid positions.

Tile Mechanics Under the Hood

Each tile is a Tile actor extending libGDX's Image class with custom drag handling. When you pick up a tile, it:

  1. Plays a pickup sound effect
  2. Renders a procedural drop shadow — a 64x64 soft-edge texture drawn at 35% alpha with an (8, -12) pixel offset, shared across all tile instances
  3. Tracks your finger position and updates the tile's screen coordinates

When you release it above the split line (the boundary between the grid and the tray), the game checks Level.getClosestOpenTargetIndex() to find the nearest unoccupied grid cell. If one exists, the tile snaps into place with a 0.15-second MoveToAction and plays a drop sound. If all positions are occupied, the tile rejects — spinning 360 degrees, shrinking back to tray size, and sliding to the bottom in a 0.13-second parallel animation.

The GWT Challenge

Getting the web build working was one of the trickier parts of the project. GWT compiles Java to JavaScript, but it only supports a subset of Java — no javax.crypto, no reflection, limited standard library. This became a problem when we added HMAC-SHA256 score signing for the leaderboard.

The solution was a pure JavaScript SHA-256 implementation embedded via GWT's JSNI (JavaScript Native Interface). The GWT leaderboard client uses RequestBuilder instead of libGDX's Gdx.net API for proper CORS handling, and the HMAC secret is embedded at compile time so it doesn't appear in the distributed JavaScript source in plain text.

Global Leaderboards

Every mode, grid size, and difficulty combination has its own leaderboard. Scores are submitted to our backend at api.such.software and ranked globally across all platforms.

Backend Architecture

The leaderboard runs on a Flask API behind Nginx on a VPS, backed by PostgreSQL. The stack is deliberately simple:

  • Flask with Gunicorn (2 workers) handles API requests
  • PostgreSQL 17 stores scores with composite indexes for fast per-mode/per-grid queries
  • Nginx terminates SSL (Let's Encrypt) and enforces rate limits: 10 requests/second globally, 6 requests/minute on score submission endpoints
  • flask-limiter adds a secondary rate limit of 100 requests/hour per IP

No ORM — just parameterized SQL queries via psycopg2. The schema is straightforward: each score row stores the device UUID, nickname, mode, grid, difficulty, primary score value, an optional secondary tiebreaker, platform, and timestamp.

Scoring Logic

Different modes rank differently:

| Mode | Primary Score | Tiebreaker | Best = | |------|--------------|------------|--------| | Classic | Completion time | — | Lowest | | Speed | Completion time | Lives remaining | Lowest time, then highest lives | | Endless | Tiles placed | Survival time | Highest tiles, then longest time | | Memory | Lives remaining | Completion time | Highest lives, then fastest time |

A UNIQUE constraint on (device_uuid, mode, grid, difficulty, score_value, score_secondary) prevents exact duplicate submissions. Conflicts upsert the nickname and platform fields so a player's display name stays current.

Anti-Cheat and Score Verification

Score submissions are signed with HMAC-SHA256. The client computes HMAC(secret, timestamp + request_body) and sends it in the X-Signature header alongside an X-Timestamp header. The server verifies the signature using constant-time comparison and rejects any request with a timestamp older than 5 minutes (replay protection).

Beyond signature verification, the server validates score bounds per mode:

  • Classic mode times must fall within plausible ranges for the grid size — you can't clear a 5x5 in 0.5 seconds
  • Speed mode lives are capped per grid and difficulty (1–6 depending on configuration)
  • Endless mode tile counts have per-grid maximums (e.g., 500 for 2x2, 200 for 5x5)
  • Memory mode lives can't exceed the configuration maximum (3–16 depending on grid and difficulty)

Scores that fail validation are rejected outright. An admin endpoint (protected by a separate API key with constant-time comparison) allows manual moderation of suspicious entries.

Privacy

Device UUIDs are never returned in API responses. When fetching a leaderboard, the client sends its own UUID as a query parameter, and the server returns an is_self boolean flag on matching entries. This lets the game highlight your own scores without exposing any player's device identifier to others.

Client Integration

The game uses a LeaderboardClient interface with two implementations:

  • HttpLeaderboardClient for Android, iOS, and Desktop — uses libGDX's Gdx.net.sendHttpRequest() with Java's javax.crypto.Mac for HMAC computation
  • GwtLeaderboardClient for the web build — uses GWT's RequestBuilder with the pure-JS SHA-256 implementation

All network calls are asynchronous with callbacks posted back to the rendering thread. Score submission is fire-and-forget: if the network call fails, the game continues normally. Local high scores always work regardless of connectivity.

Monetization

The mobile versions (Android and iOS) are free with AdMob banner ads. A one-time in-app purchase removes ads permanently and unlocks DLC tile packs. The desktop build is a paid download ($2) on itch.io with no ads. The web version is completely free with no ads.

What's Next

Since launch we've shipped Zen mode — a pressure-free creative sandbox — along with four DLC tile packs (darkly, pop, pastel, and rands) bringing the total to over 8,000 tiles. We also added a hint system for Classic mode, a milestone progression system, and expanded the soundtrack to 13 tracks with mode-specific playlists. We're continuing to explore new tile collections from art101 and gameplay ideas.

See the full Bauhaus Echo product page for more details.