Mukund Pareek / mukund pareek
  • Projects
  • Blog
  • Notes
  • Resume

MP Mukund Pareek

Unreal Engine C++ Developer

  • Projects
  • Blog
  • Notes
  • Resume

© 2026 Mukund Pareek. Built with Next.js and Tailwind CSS.

Projects
Repository Commits
November 1, 2025
Playable Games

Quick Facts

Runtime
Browser / JavaScript
Language
JavaScript, Solidity
System
Web3 / Solidity
Type
Playable Games
Status
Prototype
NeonStrike thumbnail
Playable Games

NeonStrike

Real-time browser arena shooter with a server-authoritative 66 TPS game loop, client-side prediction and reconciliation, bot AI, team mechanics, and optional on-chain match staking with signed result verification on Base Sepolia.

javascript
web3
solidity
multiplayer
browser-game

TL;DR

NeonStrike keeps real-time simulation entirely off-chain and puts only stake escrow and verifiable outcomes on-chain. The game runs at ~66 ticks/sec server-side with client prediction/reconciliation; the Web3 layer is strictly additive - staked matches and persistent stats work without impacting game-loop latency.

  • Server: Node.js + Express + Socket.IO, authoritative state, 15ms game loop
  • Client: Vanilla JS + Canvas, Socket.IO, client-side prediction with sequence-number reconciliation, ethers.js
  • Contracts: GameManager.sol (staking + ECDSA result verification), LeaderboardRegistry.sol (persistent stats/achievements), GameToken.sol (ERC-20 rewards)
  • Network: Base Sepolia testnet

Role & Scope

  • Role: Solo developer
  • Stack: JavaScript (vanilla), Node.js, Socket.IO, Solidity, Hardhat, ethers.js
  • Contracts deployed: Base Sepolia

Problem

The core design question: what is the minimum on-chain surface area that still delivers meaningful ownership and verifiability without poisoning game feel with transaction latency?

Answer: keep physics, collision, scoring, and AI entirely off-chain. Write only escrow and finalized outcomes on-chain, verified via a server ECDSA signature rather than a trusted call.

This separates the latency-sensitive gameplay loop from the latency-tolerant settlement layer.

System Architecture

NeonStrike: off-chain authoritative loop with additive on-chain staking, signed result verification, and persistent stats.

End-to-end data flow

  1. Player loads public/index.html; Socket.IO connects to backend.js
  2. Wallet connection uses EIP-6963 discovery (supports MetaMask, Rabby, Coinbase Wallet, etc.)
  3. Wallet authentication: server issues challenge nonce → client signs → server verifies signature → wallet bound to session
  4. Gameplay runs server-authoritative at 15ms tick; client sends WASD input, receives updatePlayers/updateProjectiles state, applies interpolation and prediction
  5. For staked matches: GameManager.createMatch (payable) → opponent joins with equal stake → match plays off-chain → server signs result payload → winner calls GameManager.submitResult with server signature → contract verifies ECDSA, marks complete, winner claims pot minus 2.5% fee
  6. Free-play stats flow through a separate signature-verified path into LeaderboardRegistry

Key Technical Breakdown

Server-authoritative game loop (backend.js)

The 15ms setInterval loop handles:

  • Bot AI: chase/wander state, team-based targeting, edge avoidance, shoot cooldown
  • Projectile physics and movement
  • Hit detection against player positions (with prior-position velocity estimation for smoother collision)
  • Death, respawn, elimination logic
  • Invincibility ticks - applied both on spawn and during respawn delay to prevent chain-hit edge cases
  • State broadcast to all clients

Server uses SERVER_PRIVATE_KEY only for signing result payloads - it never touches the escrow directly.

Client prediction + reconciliation (frontend.js)

Client maintains local prediction of its own movement at 15ms send cadence (matching server tick). State reconciliation uses sequence numbers:

  • incoming updatePlayers includes server sequence
  • client compares against last acknowledged sequence
  • small deltas: smooth interpolation
  • large deltas: snap correction to avoid visible rubberband

Remote players use velocity extrapolation from prior/current position pairs broadcast by the server.

Staked match lifecycle

createMatch (payable) → pending state
joinMatch (equal stake required, expiry enforced) → active
[match plays] → server signs (matchId, winner, scores, chainId)
submitResult → contract verifies ECDSA sig → completed
claimWinnings → winner receives pot − 2.5% fee
cancelMatch / claimTimeout → safety exits for pending or timed-out active matches

Replay protection is enforced via usedSignatures mapping in GameManager.

Smart contracts

GameManager.sol - escrow and staked-match lifecycle. Stake bounds: 0.001–10 ETH. Platform fee: 2.5% (platformFeeBps = 250). Owner-configurable signer key, fee, and timeout.

LeaderboardRegistry.sol - persistent stats, seasons, and achievements (first win/kill, win streaks, etc.). Normal match stats arrive via authorized call from GameManager. Free-play stats arrive via a separate signature-verified path. Owner can rotate signer and start new seasons.

GameToken.sol - ERC-20 rewards token (ARENA). Owner + authorized minters can mint; per-player daily cap; reward helpers for kills/wins/streaks.

Code Snippet

javascriptbackend.js (staked match result signing)
// Server signs match result so the winner can submit it to GameManager.sol
// without the server needing to hold escrow or call the contract directly.
// The contract verifies this ECDSA signature in submitResult().

async function signMatchResult(matchId, winnerAddress, playerScores) {
  // Encode the payload identically to how GameManager.sol decodes it.
  const messageHash = ethers.solidityPackedKeccak256(
      ["bytes32", "address", "uint256[]", "uint256"],
      [matchId, winnerAddress, playerScores, CHAIN_ID]
  );

  // Prefix with Ethereum signed message header (EIP-191).
  const signedHash = ethers.hashMessage(ethers.getBytes(messageHash));
  const signature  = await serverWallet.signMessage(ethers.getBytes(messageHash));

  return { matchId, winnerAddress, playerScores, signature };
}

// REST endpoint: winner polls for their signed result after game over.
app.get("/api/match-result/:matchId", (req, res) => {
  const result = signedResults.get(req.params.matchId);
  if (!result) return res.status(404).json({ error: "Result not ready" });
  res.json(result);
});

Video Demo

Demo: multiplayer match with bots, staking flow, and on-chain result verification.

Technical Specs

PropertyValue
Server tick rate15ms (~66.7 TPS)
Client input send rate15ms (matches server tick)
Respawn invincibility2 seconds
Shoot cooldown200ms
Match stake range0.001 – 10 ETH
Platform fee2.5% (platformFeeBps = 250)
NetworkBase Sepolia
Contract verificationBaseScan

Challenges & Solutions

Challenge 1: On-chain latency breaking post-match UX

Transaction confirmation on testnet takes 10–30 seconds. Blocking the result screen on receipt caused a dead UX pause.

Solution: Optimistic UI - display match result immediately from server state, submit the result signature asynchronously, show a confirmation indicator when the receipt arrives. Game state and settlement are fully decoupled.

Challenge 2: Wallet identity vs. gameplay identity

Using raw wallet addresses in Socket.IO payloads during gameplay exposed private information and tied gameplay identity to on-chain identity prematurely.

Solution: Server assigns an opaque session ID on connect. Wallet address is exchanged only at auth time and at match finalization - never during in-flight gameplay events.

Challenge 3: Replay attacks on signed match results

A signed result payload could be resubmitted to drain escrow a second time.

Solution: usedSignatures mapping in GameManager.sol records each consumed signature. submitResult reverts if the signature has already been used.

Known Gaps (Honest)

  • getPendingMatches() in GameManager returns an empty placeholder - off-chain indexing is expected for match discovery
  • _updateLeaderboard uses low-level call and ignores return success to prevent match-flow reverts (intentional, but worth hardening)
  • score is used as a kill proxy in several contract paths (deliberate simplification for prototype scope)
  • Duplicate WalletManager callback assignment exists in both frontend.js and gameUI.js; last-write wins depending on execution order

What I'd Improve Next

  1. Add deterministic replay/log pipeline for anti-cheat audits
  2. Formalize match indexing via subgraph or indexed backend (replace empty getPendingMatches)
  3. Add end-to-end tests across the Socket.IO + contract boundary
  4. Harden server signer key management with HSM/KMS
  5. Split monolithic frontend.js into typed modules for long-term maintainability