Skip to main content

Building a Numbers Game with React: A Step-by-Step Tutorial

 

Introduction to React Numbers Games

Numbers games are fantastic for cognitive development and entertainment. In this comprehensive React tutorial, you'll learn how to build an interactive numbers game from scratch using modern React with Hooks. This project is perfect for React beginners looking to practice state management, component composition, and local storage integration.

Learn how to create an engaging numbers game using React Hooks with scoring, graphics, and local storage persistence.

Why Build a Numbers Game with React?

  • Educational Value: Great for learning React fundamentals

  • Cognitive Benefits: Number games improve mental math skills

  • Portfolio Project: Demonstrates key React concepts in action

  • Customizable: Easy to extend with new features

Prerequisites

Before we begin, ensure you have:

  • Basic knowledge of JavaScript and React

  • Node.js installed on your system

  • A code editor (VS Code recommended)

Project Setup

Let's start by creating our React project:


npx create-react-app numbers-game
cd numbers-game
npm start

Building the Numbers Game Components

1. Main App Component with Tab Navigation


// App.js
import React, { useState, useEffect } from 'react';
import './App.css';
import GameBoard from './components/GameBoard';
import ScoreHistory from './components/ScoreHistory';
import Instructions from './components/Instructions';

function App() {
  const [activeTab, setActiveTab] = useState('game');
  const [scores, setScores] = useState([]);

  // Load scores from localStorage on component mount
  useEffect(() => {
    const savedScores = localStorage.getItem('numberGameScores');
    if (savedScores) {
      setScores(JSON.parse(savedScores));
    }
  }, []);

  // Save scores to localStorage whenever scores change
  useEffect(() => {
    localStorage.setItem('numberGameScores', JSON.stringify(scores));
  }, [scores]);

  const addScore = (newScore) => {
    const scoreWithDate = {
      ...newScore,
      date: new Date().toLocaleString(),
      id: Date.now()
    };
    setScores(prevScores => [scoreWithDate, ...prevScores].slice(0, 20)); // Keep last 20 scores
  };

  const tabs = [
    { id: 'game', label: 'Play Game' },
    { id: 'history', label: 'Score History' },
    { id: 'instructions', label: 'How to Play' }
  ];

  return (
    <div className="app">
      <header className="app-header">
        <h1>React Numbers Challenge</h1>
        <p>Test your math skills with this engaging numbers game!</p>
      </header>
      
      <nav className="tab-navigation">
        {tabs.map(tab => (
          <button
            key={tab.id}
            className={`tab-button ${activeTab === tab.id ? 'active' : ''}`}
            onClick={() => setActiveTab(tab.id)}
          >
            {tab.label}
          </button>
        ))}
      </nav>

      <main className="tab-content">
        {activeTab === 'game' && <GameBoard onScoreSubmit={addScore} />}
        {activeTab === 'history' && <ScoreHistory scores={scores} />}
        {activeTab === 'instructions' && <Instructions />}
      </main>
    </div>
  );
}

export default App;

2. Game Board Component


// components/GameBoard.js
import React, { useState, useEffect } from 'react';

const GameBoard = ({ onScoreSubmit }) => {
  const [numbers, setNumbers] = useState([]);
  const [target, setTarget] = useState(0);
  const [selectedNumbers, setSelectedNumbers] = useState([]);
  const [currentSum, setCurrentSum] = useState(0);
  const [score, setScore] = useState(0);
  const [timeLeft, setTimeLeft] = useState(60);
  const [gameStatus, setGameStatus] = useState('idle'); // idle, playing, finished
  const [message, setMessage] = useState('');

  const generateNumbers = () => {
    const newNumbers = [];
    for (let i = 0; i < 6; i++) {
      newNumbers.push(Math.floor(Math.random() * 10) + 1);
    }
    setNumbers(newNumbers);
    setTarget(Math.floor(Math.random() * 20) + 10);
  };

  const startGame = () => {
    generateNumbers();
    setSelectedNumbers([]);
    setCurrentSum(0);
    setScore(0);
    setTimeLeft(60);
    setGameStatus('playing');
    setMessage('Find number combinations that sum to the target!');
  };

  const handleNumberClick = (number, index) => {
    if (gameStatus !== 'playing') return;

    const newSelected = [...selectedNumbers, { number, index }];
    setSelectedNumbers(newSelected);
    
    const newSum = currentSum + number;
    setCurrentSum(newSum);

    if (newSum === target) {
      const points = selectedNumbers.length === 0 ? 10 : 5; // Bonus for single number matches
      const newScore = score + points;
      setScore(newScore);
      setMessage(`Perfect! +${points} points!`);
      setTimeout(() => {
        generateNumbers();
        setSelectedNumbers([]);
        setCurrentSum(0);
        setMessage('Great! Try another combination.');
      }, 1000);
    } else if (newSum > target) {
      setMessage('Too high! Try different numbers.');
      setTimeout(() => {
        setSelectedNumbers([]);
        setCurrentSum(0);
        setMessage('Select numbers that sum to the target.');
      }, 1000);
    }
  };

  const resetSelection = () => {
    setSelectedNumbers([]);
    setCurrentSum(0);
    setMessage('Selection reset. Try again!');
  };

  const endGame = () => {
    setGameStatus('finished');
    onScoreSubmit({ score, time: 60 - timeLeft });
    setMessage(`Game Over! Final Score: ${score}`);
  };

  useEffect(() => {
    let timer;
    if (gameStatus === 'playing' && timeLeft > 0) {
      timer = setTimeout(() => setTimeLeft(timeLeft - 1), 1000);
    } else if (gameStatus === 'playing' && timeLeft === 0) {
      endGame();
    }
    return () => clearTimeout(timer);
  }, [gameStatus, timeLeft]);

  const getNumberColor = (number) => {
    const colors = [
      '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', 
      '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F'
    ];
    return colors[number % colors.length];
  };

  return (
    <div className="game-board">
      <div className="game-info">
        <div className="target-display">
          <h2>Target: {target}</h2>
        </div>
        <div className="score-time">
          <div className="score">Score: {score}</div>
          <div className="timer">Time: {timeLeft}s</div>
        </div>
      </div>

      {gameStatus === 'idle' && (
        <div className="game-start">
          <button className="start-button" onClick={startGame}>
            Start Game
          </button>
        </div>
      )}

      {gameStatus !== 'idle' && (
        <>
          <div className="numbers-grid">
            {numbers.map((number, index) => {
              const isSelected = selectedNumbers.some(item => item.index === index);
              return (
                <button
                  key={index}
                  className={`number-card ${isSelected ? 'selected' : ''}`}
                  style={{ backgroundColor: getNumberColor(number) }}
                  onClick={() => handleNumberClick(number, index)}
                  disabled={isSelected || gameStatus !== 'playing'}
                >
                  {number}
                </button>
              );
            })}
          </div>

          <div className="current-selection">
            <h3>Current Selection: {selectedNumbers.map(item => item.number).join(' + ')}</h3>
            <h3>Current Sum: {currentSum}</h3>
            {selectedNumbers.length > 0 && (
              <button className="reset-button" onClick={resetSelection}>
                Reset Selection
              </button>
            )}
          </div>

          {gameStatus === 'playing' && (
            <button className="end-game-button" onClick={endGame}>
              End Game Early
            </button>
          )}
        </>
      )}

      {message && <div className="game-message">{message}</div>}

      {gameStatus === 'finished' && (
        <button className="restart-button" onClick={startGame}>
          Play Again
        </button>
      )}
    </div>
  );
};

export default GameBoard;

3. Score History Component


// components/ScoreHistory.js
import React from 'react';

const ScoreHistory = ({ scores }) => {
  const getScoreColor = (score) => {
    if (score >= 50) return '#4CAF50';
    if (score >= 30) return '#FFC107';
    if (score >= 10) return '#FF9800';
    return '#F44336';
  };

  return (
    <div className="score-history">
      <h2>Score History</h2>
      {scores.length === 0 ? (
        <p className="no-scores">No scores yet. Play the game to see your history!</p>
      ) : (
        <div className="scores-list">
          {scores.map((score, index) => (
            <div key={score.id} className="score-item">
              <div className="score-rank">#{index + 1}</div>
              <div 
                className="score-value"
                style={{ color: getScoreColor(score.score) }}
              >
                {score.score} points
              </div>
              <div className="score-time">{score.time} seconds</div>
              <div className="score-date">{score.date}</div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

export default ScoreHistory;

4. Instructions Component


// components/Instructions.js
import React from 'react';

const Instructions = () => {
  return (
    <div className="instructions">
      <h2>How to Play React Numbers Challenge</h2>
      
      <div className="instruction-section">
        <h3>🎯 Game Objective</h3>
        <p>Select numbers that add up to the target number displayed at the top of the screen.</p>
      </div>

      <div className="instruction-section">
        <h3>⭐ Scoring System</h3>
        <ul>
          <li><strong>Single Number Match:</strong> 10 points (when one number equals the target)</li>
          <li><strong>Multiple Number Combination:</strong> 5 points (when multiple numbers sum to the target)</li>
          <li><strong>Time Bonus:</strong> Higher scores for faster completions!</li>
        </ul>
      </div>

      <div className="instruction-section">
        <h3>⏰ Game Rules</h3>
        <ul>
          <li>Each game lasts 60 seconds</li>
          <li>You can select multiple numbers to form sums</li>
          <li>If your sum exceeds the target, your selection resets</li>
          <li>When you match the target exactly, you get points and new numbers appear</li>
        </ul>
      </div>

      <div className="instruction-section">
        <h3>🎮 Controls</h3>
        <ul>
          <li><strong>Click numbers</strong> to select them</li>
          <li><strong>Reset Selection</strong> to clear your current choices</li>
          <li><strong>End Game Early</strong> if you want to finish before time runs out</li>
        </ul>
      </div>

      <div className="pro-tips">
        <h3>💡 Pro Tips</h3>
        <ul>
          <li>Look for single numbers that match the target first - they give more points!</li>
          <li>Plan your combinations before clicking</li>
          <li>Practice mental math to improve your speed</li>
          <li>Check your score history to track improvement</li>
        </ul>
      </div>
    </div>
  );
};

export default Instructions;

Styling the Game

Create an App.css file with modern, responsive styling:


/* App.css */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  color: #333;
}

.app {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  min-height: 100vh;
}

.app-header {
  text-align: center;
  margin-bottom: 30px;
  color: white;
}

.app-header h1 {
  font-size: 2.5rem;
  margin-bottom: 10px;
  text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}

.app-header p {
  font-size: 1.1rem;
  opacity: 0.9;
}

/* Tab Navigation */
.tab-navigation {
  display: flex;
  background: white;
  border-radius: 10px;
  padding: 5px;
  margin-bottom: 30px;
  box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}

.tab-button {
  flex: 1;
  padding: 15px 20px;
  border: none;
  background: transparent;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s ease;
  color: #666;
}

.tab-button.active {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
  box-shadow: 0 2px 10px rgba(102, 126, 234, 0.4);
}

.tab-button:hover:not(.active) {
  background: #f8f9fa;
  color: #333;
}

/* Tab Content */
.tab-content {
  background: white;
  border-radius: 15px;
  padding: 30px;
  box-shadow: 0 10px 30px rgba(0,0,0,0.1);
  min-height: 400px;
}

/* Game Board */
.game-board {
  text-align: center;
}

.game-info {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 30px;
  padding: 20px;
  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
  border-radius: 10px;
  color: white;
}

.target-display h2 {
  font-size: 2rem;
  margin: 0;
}

.score-time {
  display: flex;
  gap: 20px;
  font-size: 1.2rem;
  font-weight: bold;
}

/* Numbers Grid */
.numbers-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 15px;
  margin-bottom: 30px;
}

.number-card {
  aspect-ratio: 1;
  border: none;
  border-radius: 15px;
  font-size: 2rem;
  font-weight: bold;
  color: white;
  cursor: pointer;
  transition: all 0.3s ease;
  box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}

.number-card:hover:not(:disabled) {
  transform: translateY(-5px);
  box-shadow: 0 8px 25px rgba(0,0,0,0.3);
}

.number-card.selected {
  transform: scale(0.9);
  opacity: 0.7;
  cursor: not-allowed;
}

.number-card:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Buttons */
.start-button, .reset-button, .end-game-button, .restart-button {
  padding: 12px 30px;
  border: none;
  border-radius: 25px;
  font-size: 1.1rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s ease;
  margin: 10px;
}

.start-button, .restart-button {
  background: linear-gradient(135deg, #4CAF50, #45a049);
  color: white;
}

.start-button:hover, .restart-button:hover {
  transform: translateY(-2px);
  box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);
}

.reset-button {
  background: linear-gradient(135deg, #FF9800, #F57C00);
  color: white;
}

.reset-button:hover {
  transform: translateY(-2px);
  box-shadow: 0 5px 15px rgba(255, 152, 0, 0.4);
}

.end-game-button {
  background: linear-gradient(135deg, #f44336, #d32f2f);
  color: white;
}

.end-game-button:hover {
  transform: translateY(-2px);
  box-shadow: 0 5px 15px rgba(244, 67, 54, 0.4);
}

/* Current Selection */
.current-selection {
  background: #f8f9fa;
  padding: 20px;
  border-radius: 10px;
  margin: 20px 0;
}

.current-selection h3 {
  margin: 10px 0;
  color: #555;
}

/* Game Message */
.game-message {
  padding: 15px;
  margin: 20px 0;
  border-radius: 10px;
  font-weight: 600;
  background: #e3f2fd;
  color: #1976d2;
  border-left: 4px solid #2196f3;
}

/* Score History */
.score-history h2 {
  text-align: center;
  margin-bottom: 30px;
  color: #333;
}

.no-scores {
  text-align: center;
  color: #666;
  font-style: italic;
  padding: 40px;
}

.scores-list {
  display: flex;
  flex-direction: column;
  gap: 15px;
}

.score-item {
  display: flex;
  align-items: center;
  padding: 15px 20px;
  background: #f8f9fa;
  border-radius: 10px;
  transition: transform 0.2s ease;
}

.score-item:hover {
  transform: translateX(5px);
  background: #e9ecef;
}

.score-rank {
  font-weight: bold;
  color: #666;
  min-width: 50px;
}

.score-value {
  font-size: 1.2rem;
  font-weight: bold;
  flex: 1;
}

.score-time, .score-date {
  color: #666;
  min-width: 120px;
  text-align: right;
}

/* Instructions */
.instructions h2 {
  text-align: center;
  margin-bottom: 30px;
  color: #333;
}

.instruction-section {
  margin-bottom: 30px;
  padding: 20px;
  background: #f8f9fa;
  border-radius: 10px;
  border-left: 4px solid #667eea;
}

.instruction-section h3 {
  margin-bottom: 15px;
  color: #333;
}

.instruction-section ul {
  list-style-type: none;
  padding-left: 0;
}

.instruction-section li {
  padding: 8px 0;
  padding-left: 25px;
  position: relative;
}

.instruction-section li:before {
  content: '✓';
  position: absolute;
  left: 0;
  color: #4CAF50;
  font-weight: bold;
}

.pro-tips {
  background: linear-gradient(135deg, #fff3cd, #ffeaa7);
  border-left: 4px solid #ffc107;
}

/* Responsive Design */
@media (max-width: 768px) {
  .app {
    padding: 10px;
  }
  
  .numbers-grid {
    grid-template-columns: repeat(2, 1fr);
  }
  
  .game-info {
    flex-direction: column;
    gap: 15px;
    text-align: center;
  }
  
  .score-item {
    flex-direction: column;
    text-align: center;
    gap: 10px;
  }
  
  .score-time, .score-date {
    text-align: center;
  }
  
  .tab-button {
    padding: 12px 10px;
    font-size: 0.9rem;
  }
}


Running the Project

Installation and Setup

  1. Create the React App:


    npx create-react-app numbers-game
    cd numbers-game
  2. Replace Default Files:

    • Replace the contents of src/App.js with our main component code

    • Replace src/App.css with our CSS styles

    • Create a src/components directory and add the three component files

  3. Project Structure:


    numbers-game/
    ├── public/
    ├── src/
    │   ├── components/
    │   │   ├── GameBoard.js
    │   │   ├── ScoreHistory.js
    │   │   └── Instructions.js
    │   ├── App.js
    │   ├── App.css
    │   └── index.js
    └── package.json
  4. Start Development Server:

    bash
    npm start
  5. Build for Production:

    bash
    npm run build

SEO Optimization Tips

To maximize your React numbers game's visibility in search results:

  1. Meta Tags: Add relevant meta descriptions in public/index.html

  2. Structured Data: Implement JSON-LD for educational games

  3. Performance: Use React.lazy() for code splitting

  4. Accessibility: Ensure proper ARIA labels and keyboard navigation

  5. Mobile Optimization: Implement responsive design as shown above

Conclusion

You've successfully built a feature-rich numbers game using React Hooks! This project demonstrates:

  • State Management with useState and useEffect

  • Component Composition and reusability

  • Local Storage Integration for data persistence

  • Responsive Design with modern CSS

  • Tab-based Navigation for better UX

  • Scoring System with history tracking

Next Steps to Enhance Your Game:

  • Add difficulty levels

  • Implement sound effects

  • Create multiplayer functionality

  • Add achievement system

  • Integrate with backend for global leaderboards

This numbers game is perfect for your React portfolio and demonstrates practical application of key React concepts. Happy coding!




Comments

Popular posts from this blog

Upload to AWS S3 from Java API

In this post, you will see code samples for how to upload a file to AWS S3 bucket from a Java Spring Boot app. The code you will see here is from one of my open-source repositories on Github, called document-sharing. Problem Let’s say you are building a document sharing app where you allow your users to upload the file to a public cloud solution. Now, let’s say you are building the API for your app with Spring Boot and you are using AWS S3 as your public cloud solution. How would you do that? This blog post contains the code that can help you achieve that. Read more below,  Upload to AWS S3 bucket from Java Spring Boot app - My Day To-Do (mydaytodo.com)

Addressing app review rejections for auto-renewing subscription in-app purchase (iOS)

The ability to know what the weather is like while planning your day is a feature of  My Day To-Do  Pro and as of the last update it’s also a part of the  Lite version . Unlike the Pro version it’s an auto-renewing subscription based  in-app purchase (IAP)  in the Lite version. What means is that when a user purchases it, the user only pays for the subscription duration after which the user will be automatically charged for the next period. Adding an  auto-renewing  subscription based IAP proved to be somewhat challenging in terms of the app store review i.e. the app update was rejected by the App Review team thrice because of missing information about the IAP. Therefore in this post I will share my experiences and knowledge of adding auto-renewing IAP in hopes to save someone else the time that I had to spend on this problem. In-App purchase This year I started adding IAPs to My Day To-Do Lite which lead to learning about different types of IAP...

Serving HTML content in an iOS app that works in iOS 7 and later (using Swift)

As I have mentioned in an earlier post , I really enjoying coding in Swift. Now what am I doing with it? Well I am trying to build an HTML5 app that must work on devices with iOS 7. So in iOS8 apple has introduced a whole bunch of features that facilitate easy communication between web content and lets just call it back-end Swift code, but those features are not in iOS 7. So why do I want to build something that would work in an older OS? well I do not expect existing iOS users to upgrade to iOS 8 straight away and i also know a couple of people who would be very reluctant to upgrade their iPhones to iOS 8. Now in case you do not, you can have a read of the "Working with WebViews" section of this post , to know how to serve HTML content with WebViews. So when I started building my app, I wanted to know: How do I invoke some Swift code from my HTML content? Well the solution to this may feel a little bit "hacky" but it is a solution to achieve this.  The followi...