A Street Fighter themed slot machine game built as an Electron desktop application using React and TypeScript.
- 4-Reel Slot Machine: Spin 4 reels featuring 16 iconic Street Fighter characters (Ryu, Ken, Chun-Li, Blanka, Guile, Zangief, Dhalsim, E. Honda, Balrog, Vega, Sagat, M. Bison, Cammy, Fei Long, T. Hawk, and Dee Jay)
- Coin System: Start with 100 coins, bet 10 coins per spin
- Reward System:
- Pair (2 matching symbols): 20 coins
- Three-of-a-Kind (3 matching symbols): 50 coins
- Perfect Match (all 4 symbols match): 100 coins
- Rewards accumulate (e.g., 2 pairs = 40 coins, pair + three-of-a-kind = 70 coins)
- Auto-Spin: Enable continuous automatic spinning until you run out of coins or manually stop
- Game Over: When coins drop below the bet amount, the game ends with a special game over screen
- Add Players: Create new players with custom names
- Leaderboard: View all players with their highest balance and total spins
- Player Persistence: All player data is stored locally in a SQLite database
- Sound Effects: Authentic Street Fighter sound effects including:
- Game start music and round 1 theme
- Symbol selection sounds
- Win/loss sound effects
- Perfect win celebration
- Game over sequence
- Visual Feedback:
- Animated slot reels with sequential stopping
- Highlighted winning symbols
- Reward modal showing winnings
- Retro-styled UI with Street Fighter aesthetic
- Game Session Tracking: Each game session is tracked with:
- Start and end times
- Starting and ending coin balances
- Individual spin records with symbols, bet amounts, and winnings
- Local Database: SQLite database stores all game data locally
- Cross-Platform: Build for Windows, macOS, and Linux
- Modern Stack: Built with Electron, React, TypeScript, and Vite
- Node.js: Version 18 or higher
- Yarn: Package manager (install via
npm install -g yarn)
yarnyarn run devThis will:
- Start the Electron application in development mode
- Enable hot-reload for React components
- Open the game window automatically
- Add a Player: Enter your name in the input field and click "Add Player"
- Start Playing: Click "Play" next to your name in the players list
- Spin: Click the "Spin" button to play (costs 10 coins)
- Auto-Spin: Enable "Auto-Spin" for continuous gameplay
- Restart: Click "Restart" to start a new game with 100 coins
- Quit: Click "Quit" to return to the home screen
yarn build:winyarn build:macyarn build:linuxBuilt applications will be available in the dist/ directory.
yarn dev- Start development serveryarn build- Build the application (runs type checking)yarn start- Preview the built applicationyarn lint- Run ESLintyarn format- Format code with Prettieryarn typecheck- Run TypeScript type checking
src/
├── main/ # Electron main process (database, IPC handlers)
├── preload/ # Preload scripts for secure IPC communication
└── renderer/ # React application (UI components)
└── src/
├── components/ # React components
├── assets/ # Images, sounds, styles
└── lib/ # Utility functions
The game uses SQLite to store:
- Players: Name, creation date, highest balance, total spins
- Games: Game sessions with start/end times and balances
- Spins: Individual spin records with symbols, bets, and winnings
The database file is stored in the Electron app's user data directory.
This section explains key architectural and implementation decisions made during development, along with their trade-offs.
Decision: Separated main process (Node.js) and renderer process (React) with IPC communication via preload scripts.
Rationale:
- Security: Context isolation prevents renderer from accessing Node.js APIs directly
- Performance: Main process handles database operations without blocking UI
- Separation of concerns: Business logic (database) separate from presentation (React)
Trade-offs:
- ✅ Better security and performance isolation
- ✅ Follows Electron security best practices
- ❌ More complex than a single-process app
- ❌ Requires IPC boilerplate for all main-renderer communication
Decision: Enabled contextIsolation: true and used contextBridge for secure IPC.
Rationale:
- Prevents renderer from accessing Node.js globals directly
- Required for modern Electron security best practices
- Allows controlled API exposure
Trade-offs:
- ✅ Enhanced security against XSS attacks
- ✅ Future-proof against Electron security changes
- ❌ Requires explicit API definition in preload scripts
- ❌ Slightly more setup than disabling context isolation
Decision: Used React's built-in useState and useEffect hooks instead of Redux, Zustand, or other state management libraries.
Rationale:
- Simplicity: No additional dependencies or boilerplate
- Sufficient for current app complexity
- Easier for developers unfamiliar with state management libraries
Trade-offs:
- ✅ Simpler codebase, easier to understand
- ✅ No external dependencies
- ✅ Faster initial development
- ❌ Could become unwieldy with more complex state interactions
- ❌ No built-in devtools for state debugging
- ❌ Prop drilling in deeply nested components
Future Consideration: If the app grows significantly, consider Zustand or Jotai for lightweight state management.
Decision: Used better-sqlite3 (synchronous SQLite) instead of async alternatives like sql.js or node-sqlite3.
Rationale:
- Performance: Synchronous operations are faster for local desktop apps
- Simplicity: No need for async/await in database code
- Reliability: better-sqlite3 is well-maintained and performant
- Native bindings: Better performance than JavaScript implementations
Trade-offs:
- ✅ Excellent performance for local desktop app
- ✅ Simpler code (no async/await needed)
- ✅ Native C++ bindings for speed
- ❌ Blocks main thread (acceptable for desktop app with IPC)
- ❌ Requires native compilation (handled by electron-builder)
Decision: Enabled Write-Ahead Logging (WAL) mode for SQLite.
Rationale:
- Better concurrent read performance
- Faster writes (sequential log writes)
- Allows readers to not block writers
Trade-offs:
- ✅ Better performance for concurrent operations
- ✅ Faster writes
- ❌ Slightly more complex database file structure (WAL file)
- ❌ Requires proper cleanup (handled by SQLite)
Decision: Used normalized schema with separate tables for players, games, and spins with foreign key constraints.
Rationale:
- Data integrity: Foreign keys prevent orphaned records
- Query flexibility: Easy to aggregate statistics
- Scalability: Can handle complex queries efficiently
Trade-offs:
- ✅ Data integrity and consistency
- ✅ Flexible querying and reporting
- ✅ Easy to extend with new features
- ❌ More complex than a single-table approach
- ❌ Requires JOINs for some queries (acceptable trade-off)
Decision: Store database in Electron's user data directory instead of app directory.
Rationale:
- Persistence: Survives app updates
- User-specific: Each user has their own data
- OS-appropriate: Follows platform conventions
Trade-offs:
- ✅ Data persists across app updates
- ✅ Follows OS conventions
- ✅ User-specific data isolation
- ❌ Requires app permissions (handled automatically)
Decision: Calculate rewards in the renderer process (Play.tsx) instead of main process.
Rationale:
- Immediate feedback: No IPC delay
- Simpler code: All game logic in one place
- Acceptable for single-player game (no cheating concerns)
Trade-offs:
- ✅ Instant UI updates
- ✅ Simpler code organization
- ✅ Better user experience
- ❌ Not suitable for multiplayer (could be manipulated)
- ❌ Logic duplication if server-side validation needed later
Note: For a multiplayer or online game, reward calculation should be server-side.
Decision: Reels stop sequentially (500ms apart) instead of simultaneously.
Rationale:
- Better UX: Creates anticipation and excitement
- More realistic: Mimics physical slot machines
- Visual feedback: Users can see each symbol revealed
Trade-offs:
- ✅ Enhanced user experience
- ✅ More engaging gameplay
- ✅ Better visual feedback
- ❌ Longer spin duration (2 seconds total)
- ❌ More complex state management
Decision: Used electron-vite instead of webpack or other bundlers.
Rationale:
- Faster builds: Vite's esbuild-based bundling
- Better DX: Faster HMR and dev server
- Simpler config: Less boilerplate than webpack
- Modern: Built for modern tooling
Trade-offs:
- ✅ Much faster development builds
- ✅ Excellent HMR experience
- ✅ Simpler configuration
- ❌ Less mature ecosystem than webpack
- ❌ Fewer plugins available
Decision: Fixed window size (900x720) instead of resizable.
Rationale:
- Consistent UI: Prevents layout issues
- Game design: Slot machine UI works best at fixed size
- Simpler: No responsive design needed
Trade-offs:
- ✅ Consistent user experience
- ✅ Simpler CSS and layout
- ✅ No responsive design complexity
- ❌ Less flexible for users
- ❌ Doesn't adapt to different screen sizes
Decision: Used browser Audio API directly instead of a sound management library.
Rationale:
- Simplicity: No additional dependencies
- Sufficient for current needs
- Lightweight: No library overhead
Trade-offs:
- ✅ No external dependencies
- ✅ Simple implementation
- ✅ Full control over audio playback
- ❌ No built-in volume management
- ❌ Manual error handling required
- ❌ Could benefit from audio pooling for performance
Future Consideration: If sound management becomes complex, consider howler.js or similar library.
Decision: Used React Router's MemoryRouter instead of BrowserRouter.
Rationale:
- Desktop app: No URL bar, no need for browser history
- Simpler: No hash or path routing complexity
- Appropriate: Electron apps typically use memory routing
Trade-offs:
- ✅ Appropriate for desktop app
- ✅ Simpler routing logic
- ✅ No URL manipulation concerns
- ❌ No deep linking support (not needed for this app)
- ❌ No browser back/forward (not applicable)
Decision: Used TypeScript for all code (main, preload, renderer).
Rationale:
- Type safety: Catch errors at compile time
- Better DX: IDE autocomplete and refactoring
- Maintainability: Self-documenting code
Trade-offs:
- ✅ Compile-time error detection
- ✅ Better IDE support
- ✅ Easier refactoring
- ❌ Slightly slower development (type annotations)
- ❌ Build step required (handled by tooling)
Decision: Used functional components exclusively with React hooks.
Rationale:
- Modern React: Recommended approach
- Simpler: Less boilerplate than class components
- Hooks: Better code reuse and organization
Trade-offs:
- ✅ Modern React best practices
- ✅ Less boilerplate
- ✅ Better performance (with React optimizations)
- ❌ Learning curve for developers unfamiliar with hooks
- ❌ Requires careful dependency arrays in useEffect
Decision: Used SQLite prepared statements for all queries.
Rationale:
- Performance: Compiled queries are faster
- Security: Prevents SQL injection
- Best practice: Standard database practice
Trade-offs:
- ✅ Better performance
- ✅ SQL injection protection
- ✅ Industry best practice
- ❌ Slightly more verbose code
- ❌ Requires statement management
Decision: Imported symbol images as URLs using Vite's asset handling.
Rationale:
- Build optimization: Vite handles asset optimization
- Caching: Browser caching benefits
- Simplicity: Standard web asset handling
Trade-offs:
- ✅ Build-time optimization
- ✅ Browser caching
- ✅ Standard approach
- ❌ Multiple HTTP requests (mitigated by bundling)
- ❌ Could use sprite sheets for fewer requests (future optimization)