Inspiration

The idea came off a phone screen, not a shelf. We were mid-build on other games when a video showed up — two people at a real board, pulling pucks back against a band and firing them at each other, no turns, both going at once. We went looking for it on the Play Store expecting a dozen versions already. There were barely any. That gap was the pitch: a game everyone gets in five seconds, and almost nobody had shipped well on mobile.

The rule came before most of the code. A new table has to force a new player decision. Not easier, not harder, different. If two tables stop a puck in the same place for the same pull, they're one table with two paint jobs.

What it does

Two players, one board, no turns. You both play at the same time.

Every puck on your half has to go through a gap in the centre divider and land on the other player's side. The first player with an empty half wins. You have to hold it empty for a moment, so a puck cannot sneak back at the last second and steal it from you.

Shooting is the part worth explaining. You can drag a puck anywhere on your half, but dragging never fires it. To shoot, you pull the puck back into the rubber band along your edge and let go. The band fires dead straight, so the aiming happens before you pull rather than during. A longer pull is always more power. That never changes from table to table, because the band is equipment and the board is what a table changes.

A table is made of three things that combine freely. The profile is everything that moves the puck: friction, damping, gravity, and any force the board applies. The layout is the board's shape: one gap in the middle, two gaps at the sides, or a gap that slides across the divider so you have to time the shot. The skin is what you look at.

Three ways to play, side by side: against a bot, on one phone with a player at each end, or on two phones on the same network. No account and no internet. The bot isn't a difficulty slider — it's a single skill dial that nudges itself around a target after every shot, aiming to be competitive rather than perfect or rubber-banded to let you win.

How we built it

Flutter and Flame, with Forge2D underneath. Pucks collide and rebound for real rather than following an animation.

Nothing on the board is a sprite. Each skin has a fragment shader for the board and another for the pucks. Every table's controls are cut from the same kind of object — a key standing physically off the board, lit and glossy on top, with a shadow it casts on the table under it — but each table cuts its keys from a different stock: timber on wood, brushed metal on flux and zero-g, fractured crystal on ice. One consistent way of building a button, carrying six different materials.

The home screen isn't static either. Two pucks play a small, physics-driven duel of their own behind the menu — real damping and restitution from whichever table's profile is selected, with a different random opening every time and no player input involved.

Over LAN, one phone is the authoritative host and streams match state to the other. It was built and played on two handsets rather than only in a simulator, which is where most of its bugs came from.

Purchases run through RevenueCat, and so do the rewarded ads — AdMob serves them; RevenueCat Ads tracks them.

Challenges we ran into

A football table was requested, and the code couldn't answer.

At that point, a table's physics and its artwork were the same object. So the question "what decision does a football pitch force?" had no answer, and a football pitch could not exist. It was never a physics request. It was wood physics with turf painted on it.

Splitting that object in two fixed it. Physics went one way into a profile, art the other way into a skin, and a skin declares which profile it rides. Football rides wood; snow rides ice. Layouts were already separate and did not move. The decision rule still applies to profiles and layouts, because those are what a player has to think about. It does not apply to a skin, because nobody asks what decision a shirt forces.

The second problem showed up over LAN, and it was the opposite kind — everything looked fine. 290+ tests green, analyser clean, and real bugs shipping anyway: an aim arrow that never drew on the client's screen, a drag rule checking ownership where it needed halves, the camera rotating the wrong screen, a guest hanging up and leaving the host stuck mid-round, a release build R8 killed before any Dart code ran. None of it showed up in a simulator. A loopback socket doesn't drop packets or block a broadcast, and both ends share a clock — real bugs needed real Wi-Fi and two actual phones to exist. Lesson stuck: a green suite means nothing for networked code until it's run on hardware.

The bot taught the same lesson a third way. The first version shipped with a skill dial that visibly did nothing — a bot at its weakest setting played identically to one at its strongest. The cause was two numbers that looked unrelated: the bot's hand considered a shot "arrived" at a tolerance tighter than the gap it was aiming through actually needed, so the skill value never got the chance to matter. Watching it play never surfaced that. Measuring the actual aim spread against the gap did.

Accomplishments that we're proud of

Splitting a table into a profile and a skin. Before that, "build a football table" was a request with no answer, because physics and art were the same object. After it, football rides wood's physics and snow rides ice's, and neither one cost a new mechanic — just art. It turned one feature request the code couldn't answer into two shipped tables the same week, with room for more.

What we learned

We built six mechanics on the Flux table, measured them, and took them out. A pull, a curve, a bend, a field, a charge. Every one of them acted on the puck after it had left the band, and by then the player had already made every choice they got to make. So each one arrived as something to watch rather than something to answer. None of them was a tuning problem, which is why tuning never rescued any of them.

The same test now applies to artwork. A skin does not have to force a decision, but it does have to be a place somebody has stood, with props you would know at arm's length. Three Zero-G skins failed it and were dropped: a neon grid, a station interior, a moonscape. Seen from directly overhead, there was nothing familiar to hang the art on. Football and snow work because a fence, a pine and a hoarding read instantly.

The bot added a third rule to the same family: don't trust a change because it looked right when you played it. A skill dial that felt fine in a few rounds was still broken underneath, and the fix only showed up once the numbers were measured rather than felt.

What's next

More skins, and more profiles behind them — the split makes both cheap now. Online multiplayer beyond LAN, if the reception justifies building past "two phones on the same network."

Built With

  • admob
  • android
  • dart
  • flame
  • flutter
  • forge2d
  • fragment-shaders
  • glsl
  • google-mobile-ads
  • google-play
  • haptics
  • in-app-purchases
  • lan-multiplayer
  • revenuecat
  • revenuecat-ads
  • rewarded-ads
  • shared-preferences
+ 17 more
Share this project:

Updates

Submission history