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...

Getting started with iOS programming using Swift (Part 1)

I have not been too fond of Objective-C, which was the primary reason for me to stay away from making iOS apps till now. So what changed? Well Apple has done something very interesting recently and that is the introduction of a new programming language i.e. Swift. Swift is awesome, it almost feels like Python, C++ and Objective-C had a baby with some of their good parts in them. So I have been getting to know Swift and it is an awesome language to program in. What I am going to share with this and a series of blog posts are solutions to some problems that i have encounter while i am trying to finish my first iOS app. The one hurdle that I have encountered while getting started on developing an iOS app is that a majority of the solutions for iOS specific problems provide solutions to them using Objective-C. Which is fair, because Swift has not been around for that long. Anyway let us get started with a few basics, A few basics I would highly recommend having a read of this book...