EigenInteractive

EigenInteractive Flutter

The Flutter half of EigenInteractive: a server-authoritative engine for turn-based multiplayer games.

eigen_client is the pure Dart protocol, domain, clock, and live-session runtime. eigen_flutter adds provider-neutral Flutter integration and reusable presentation without owning a root app. The complete first-party product lives in eigen_shell, and Firebase is an optional sibling adapter in eigen_firebase. A game supplies a small GameModule containing its client-side rules and presentation.

Start a game

The recommended flow creates the Cloudflare Worker and Flutter app together:

pnpm create eigen-game my-game
# or
npm create eigen-game@latest my-game

The scaffold installs published npm/pub.dev dependencies; it does not clone the engine repositories. For an existing app or separate Worker/app repositories, follow the manual setup guide.

Add to an app

dependencies:
  eigen_flutter: ^0.9.0
  eigen_shell: ^0.1.0 # omit when embedding beneath your own app root
  eigen_firebase: ^0.2.0 # only when the app chooses Firebase
  firebase_core: ^4.9.0

Import the framework through its public barrel:

import 'package:eigen_flutter/eigen_flutter.dart';
import 'package:eigen_firebase/eigen_firebase.dart';
import 'package:eigen_shell/eigen_shell.dart';

Do not depend on eigen_api directly or deep-import core/, features/, or shared/. The barrel is the supported game-facing API.

On Android, the Firebase adapter's local-notification dependency requires core-library desugaring in the application module. create-eigen-game configures it automatically. For a hand-created app, add this to android/app/build.gradle.kts:

android {
    compileOptions {
        isCoreLibraryDesugaringEnabled = true
    }
}

dependencies {
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}

Boot the app with your module, branding, Worker origin, and generated Firebase configuration. The standard app targets Android and web, so the public Web Push key is required deployment configuration; it belongs to the same Firebase project used for authentication:

const apiBaseUrl = String.fromEnvironment('API_BASE_URL');
const googleWebClientId = String.fromEnvironment('GOOGLE_WEB_CLIENT_ID');
const firebaseVapidKey = String.fromEnvironment('FIREBASE_VAPID_KEY');
const appHost = String.fromEnvironment('APP_HOST');

Future<void> main() => runEigenShell(
  module: const MyGameModule(),
  config: AppConfig(
    branding: const Branding(appName: 'My Game', seedColor: Colors.indigo),
    engine: EngineConfig(
      apiBaseUrl: apiBaseUrl,
      appHost: appHost.isEmpty ? null : appHost,
    ),
  ),
  initializeAdapter: () => initializeEigenFirebase(
    firebaseOptions: DefaultFirebaseOptions.currentPlatform,
    firebase: FirebaseAdapterConfig(
      googleWebClientId: googleWebClientId,
      vapidKey: firebaseVapidKey,
    ),
    onBackgroundMessage: onBackgroundMessage,
    telemetry: FirebaseTelemetryPolicy.releaseOnly(),
  ),
);

Without Firebase, omit eigen_firebase and call runEigenShell without an initializer. To embed Eigen into an existing application, omit eigen_shell too and install EigenFlutterScope beneath your own MaterialApp or WidgetsApp. Auth, notification, and analytics ports default to unavailable/no-op implementations; another adapter can override them through package:eigen_flutter/adapters.dart.

These are public build-time values. Scaffolded apps keep them in app-config.json and use the same command option for Android and web:

flutter run --dart-define-from-file=app-config.json

Missing or malformed required values are reported together before their services start. Keep actual secrets on the Worker. Telemetry collection is off unless the app passes an explicit policy such as releaseOnly().

Connect a game app to Firebase with the package executable:

dart run eigen_firebase:configure_firebase

It runs FlutterFire for Android and web, then derives web/firebase-config.js for the messaging service worker from the Web app FlutterFire selected. The public VAPID key remains in app-config.json because it is not part of Firebase's app SDK configuration.

The game boundary

The authoritative TypeScript module declares state, observation, action, and config once. It emits game-contract.json; the development-only eigen_codegen package turns that artifact into immutable Dart payloads, a typed rules base, and fixture copies:

flutter pub add --dev eigen_codegen
dart run eigen_codegen:generate_payloads \
  --contract ../server/game-contract.json \
  --output lib/game/generated/payloads.dart \
  --fixtures-output test/fixtures

Your handwritten Dart code then:

  • extends the generated V<N>RulesBase;
  • implements client-side legality and optional optimistic preview;
  • renders the game from GameContentContext;
  • registers one rules unit per server schemaVersion;
  • declares the version-independent creation and rules UI.

The server remains authoritative. Shared fixtures run against both languages so payload and behavior drift fails in tests.

Example

example/ is a complete Rock–Paper–Scissors client. It deliberately uses simultaneous hidden commitments to demonstrate per-seat observations and the valid “do not predict this move” path:

cd example
flutter pub get
flutter test
flutter build web --release

The package treats Android and web as supported targets. The generated scaffold includes the browser Firebase Messaging service worker, Firebase Auth's web popup flow, cross-origin Worker setup, and a release web build in CI. See Deploy the web app.

Documentation

Working on the framework

  • CONTRIBUTING.md: local setup, generation, validation, changelog entries, and pull requests.
  • MAINTAINERS.md: pub.dev setup, releases, version tags, and failure recovery.

Libraries

eigen_flutter
Reusable Flutter integration for EigenInteractive game applications.
testing/twin_fixtures
Twin-drift fixture runner: the Dart half of the shared JSON fixtures that keep a version unit's TS and Dart GameRules twins in sync.