diff --git a/puzzle-game/.gitignore b/puzzle-game/.gitignore new file mode 100644 index 000000000..d572f5f4e --- /dev/null +++ b/puzzle-game/.gitignore @@ -0,0 +1,33 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/puzzle-game/LICENSE b/puzzle-game/LICENSE new file mode 100644 index 000000000..807c30be7 --- /dev/null +++ b/puzzle-game/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Next.js Puzzle Game + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/puzzle-game/README.md b/puzzle-game/README.md new file mode 100644 index 000000000..8ffb3e958 --- /dev/null +++ b/puzzle-game/README.md @@ -0,0 +1,65 @@ +# Next.js Sliding Puzzle Game + +A simple sliding puzzle game built with Next.js for Windows. + +## Features + +- Three difficulty levels: Easy (3x3), Medium (4x4), and Hard (5x5) +- Timer and move counter to track your progress +- Responsive design that works on all screen sizes +- Optimized for Windows desktop environments + +## Getting Started + +### Prerequisites + +- Node.js 14.0 or later +- npm or yarn + +### Installation + +1. Clone this repository or download the source code +2. Navigate to the project directory +3. Install dependencies: + +```bash +npm install +# or +yarn install +``` + +### Development + +Run the development server: + +```bash +npm run dev +# or +yarn dev +``` + +Open [http://localhost:3000](http://localhost:3000) in your browser to see the game. + +### Building for Windows + +To create a static build that can be used in a Windows application: + +```bash +npm run build +# or +yarn build +``` + +This will generate a static export in the `out` directory that can be served as a static site or embedded in a Windows application. + +## How to Play + +1. Select a difficulty level (Easy, Medium, or Hard) +2. Click "Start Game" to begin +3. Click on tiles adjacent to the empty space to move them +4. Arrange the tiles in numerical order to win +5. Try to complete the puzzle in the fewest moves and shortest time! + +## License + +This project is open source and available under the [MIT License](LICENSE). \ No newline at end of file diff --git a/puzzle-game/components/GameControls.tsx b/puzzle-game/components/GameControls.tsx new file mode 100644 index 000000000..676793025 --- /dev/null +++ b/puzzle-game/components/GameControls.tsx @@ -0,0 +1,79 @@ +import React from 'react'; + +interface GameControlsProps { + moves: number; + time: number; + onReset: () => void; +} + +const GameControls: React.FC = ({ moves, time, onReset }) => { + // Format time as MM:SS + const formatTime = (seconds: number): string => { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; + }; + + return ( +
+
+
+ Moves: + {moves} +
+
+ Time: + {formatTime(time)} +
+
+ + + +
+ ); +}; + +export default GameControls; \ No newline at end of file diff --git a/puzzle-game/components/PuzzleBoard.tsx b/puzzle-game/components/PuzzleBoard.tsx new file mode 100644 index 000000000..4ffd51401 --- /dev/null +++ b/puzzle-game/components/PuzzleBoard.tsx @@ -0,0 +1,192 @@ +import { useState, useEffect } from 'react'; + +interface PuzzleBoardProps { + difficulty: 'easy' | 'medium' | 'hard'; + onMove: () => void; + onWin: () => void; +} + +const PuzzleBoard: React.FC = ({ difficulty, onMove, onWin }) => { + // Set grid size based on difficulty + const gridSize = difficulty === 'easy' ? 3 : difficulty === 'medium' ? 4 : 5; + + // State for the puzzle tiles + const [tiles, setTiles] = useState([]); + const [emptyIndex, setEmptyIndex] = useState(0); + + // Initialize the puzzle + useEffect(() => { + initializePuzzle(); + }, [difficulty]); + + // Check for win condition + useEffect(() => { + if (tiles.length === 0) return; + + const isWin = checkWinCondition(); + if (isWin) { + onWin(); + } + }, [tiles]); + + // Initialize the puzzle with shuffled tiles + const initializePuzzle = () => { + const totalTiles = gridSize * gridSize; + const newTiles = Array.from({ length: totalTiles - 1 }, (_, i) => i + 1); + newTiles.push(0); // Add empty tile (represented by 0) + + // Shuffle the tiles (ensuring it's solvable) + const shuffledTiles = shuffleTiles(newTiles); + + setTiles(shuffledTiles); + setEmptyIndex(shuffledTiles.indexOf(0)); + }; + + // Shuffle the tiles while ensuring the puzzle is solvable + const shuffleTiles = (tiles: number[]): number[] => { + const shuffled = [...tiles]; + let currentIndex = shuffled.length; + + // Fisher-Yates shuffle algorithm + while (currentIndex !== 0) { + const randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex--; + + [shuffled[currentIndex], shuffled[randomIndex]] = + [shuffled[randomIndex], shuffled[currentIndex]]; + } + + // Check if the puzzle is solvable + if (isSolvable(shuffled)) { + return shuffled; + } else { + // If not solvable, swap two tiles to make it solvable + if (shuffled[0] !== 0 && shuffled[1] !== 0) { + [shuffled[0], shuffled[1]] = [shuffled[1], shuffled[0]]; + } else { + [shuffled[shuffled.length - 1], shuffled[shuffled.length - 2]] = + [shuffled[shuffled.length - 2], shuffled[shuffled.length - 1]]; + } + return shuffled; + } + }; + + // Check if the puzzle is solvable + const isSolvable = (tiles: number[]): boolean => { + // Count inversions + let inversions = 0; + const tilesWithoutEmpty = tiles.filter(tile => tile !== 0); + + for (let i = 0; i < tilesWithoutEmpty.length; i++) { + for (let j = i + 1; j < tilesWithoutEmpty.length; j++) { + if (tilesWithoutEmpty[i] > tilesWithoutEmpty[j]) { + inversions++; + } + } + } + + // For odd grid sizes, the puzzle is solvable if inversions is even + if (gridSize % 2 === 1) { + return inversions % 2 === 0; + } + // For even grid sizes, the puzzle is solvable if: + // (inversions + row of empty from bottom) is odd + else { + const emptyTileIndex = tiles.indexOf(0); + const emptyTileRow = Math.floor(emptyTileIndex / gridSize); + const rowFromBottom = gridSize - emptyTileRow; + return (inversions + rowFromBottom) % 2 === 1; + } + }; + + // Check if the puzzle is solved + const checkWinCondition = (): boolean => { + for (let i = 0; i < tiles.length - 1; i++) { + if (tiles[i] !== i + 1) { + return false; + } + } + return tiles[tiles.length - 1] === 0; + }; + + // Handle tile click + const handleTileClick = (index: number) => { + if (!isMovable(index)) return; + + const newTiles = [...tiles]; + newTiles[emptyIndex] = newTiles[index]; + newTiles[index] = 0; + + setTiles(newTiles); + setEmptyIndex(index); + onMove(); + }; + + // Check if a tile is movable (adjacent to the empty tile) + const isMovable = (index: number): boolean => { + // Check if the tile is in the same row and adjacent column + const sameRow = Math.floor(index / gridSize) === Math.floor(emptyIndex / gridSize); + const adjacentCol = Math.abs((index % gridSize) - (emptyIndex % gridSize)) === 1; + + // Check if the tile is in the same column and adjacent row + const sameCol = (index % gridSize) === (emptyIndex % gridSize); + const adjacentRow = Math.abs(Math.floor(index / gridSize) - Math.floor(emptyIndex / gridSize)) === 1; + + return (sameRow && adjacentCol) || (sameCol && adjacentRow); + }; + + return ( +
+ {tiles.map((tile, index) => ( +
handleTileClick(index)} + > + {tile !== 0 && tile} +
+ ))} + + +
+ ); +}; + +export default PuzzleBoard; \ No newline at end of file diff --git a/puzzle-game/next.config.js b/puzzle-game/next.config.js new file mode 100644 index 000000000..73bb93ebc --- /dev/null +++ b/puzzle-game/next.config.js @@ -0,0 +1,10 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + output: 'export', // Enable static exports for Windows desktop app + images: { + unoptimized: true, // Required for static export + }, +}; + +module.exports = nextConfig; \ No newline at end of file diff --git a/puzzle-game/package-for-windows.js b/puzzle-game/package-for-windows.js new file mode 100644 index 000000000..1a65b71dd --- /dev/null +++ b/puzzle-game/package-for-windows.js @@ -0,0 +1,107 @@ +const { exec } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +console.log('Starting Windows packaging process...'); + +// Step 1: Build the Next.js application +console.log('Building Next.js application...'); +exec('npm run build', (error, stdout, stderr) => { + if (error) { + console.error(`Build error: ${error.message}`); + return; + } + if (stderr) { + console.error(`Build stderr: ${stderr}`); + } + console.log(`Build output: ${stdout}`); + + // Step 2: Create a simple HTML launcher + console.log('Creating Windows launcher...'); + const launcherContent = ` + + + + Puzzle Game Launcher + + + +
+

Next.js Sliding Puzzle Game

+

Click the button below to launch the game

+ +
+ + + + + `; + + fs.writeFileSync('launcher.html', launcherContent); + console.log('Launcher created: launcher.html'); + + // Step 3: Create a README for Windows users + const windowsReadmeContent = ` +# Next.js Sliding Puzzle Game for Windows + +## How to Run + +1. Double-click on "launcher.html" to open the launcher +2. Click the "Launch Game" button to start the game +3. Enjoy playing! + +## Troubleshooting + +If the game doesn't launch: +- Make sure you have a modern web browser installed (Chrome, Firefox, Edge) +- Check that the "out" folder exists and contains the game files +- If you're still having issues, try opening "out/index.html" directly in your browser + +## About + +This is a sliding puzzle game built with Next.js. The goal is to arrange the tiles in numerical order. + `; + + fs.writeFileSync('WINDOWS_README.txt', windowsReadmeContent); + console.log('Windows README created: WINDOWS_README.txt'); + + console.log('Packaging complete! The game can now be distributed for Windows.'); +}); \ No newline at end of file diff --git a/puzzle-game/package.json b/puzzle-game/package.json new file mode 100644 index 000000000..b1281f533 --- /dev/null +++ b/puzzle-game/package.json @@ -0,0 +1,23 @@ +{ + "name": "nextjs-puzzle-game", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "package-windows": "node package-for-windows.js" + }, + "dependencies": { + "next": "^14.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/node": "^20.8.9", + "@types/react": "^18.2.33", + "@types/react-dom": "^18.2.14", + "typescript": "^5.2.2" + } +} \ No newline at end of file diff --git a/puzzle-game/pages/_app.tsx b/puzzle-game/pages/_app.tsx new file mode 100644 index 000000000..9fa223911 --- /dev/null +++ b/puzzle-game/pages/_app.tsx @@ -0,0 +1,6 @@ +import type { AppProps } from 'next/app'; +import '../styles/globals.css'; + +export default function App({ Component, pageProps }: AppProps) { + return ; +} \ No newline at end of file diff --git a/puzzle-game/pages/index.tsx b/puzzle-game/pages/index.tsx new file mode 100644 index 000000000..aa2415a64 --- /dev/null +++ b/puzzle-game/pages/index.tsx @@ -0,0 +1,142 @@ +import { useState, useEffect } from 'react'; +import Head from 'next/head'; +import PuzzleBoard from '../components/PuzzleBoard'; +import GameControls from '../components/GameControls'; + +export default function Home() { + const [gameStarted, setGameStarted] = useState(false); + const [difficulty, setDifficulty] = useState<'easy' | 'medium' | 'hard'>('easy'); + const [moves, setMoves] = useState(0); + const [time, setTime] = useState(0); + const [isWin, setIsWin] = useState(false); + + // Reset timer when game is won + useEffect(() => { + if (isWin) return; + + let timer: NodeJS.Timeout; + if (gameStarted) { + timer = setInterval(() => { + setTime(prevTime => prevTime + 1); + }, 1000); + } + + return () => { + if (timer) clearInterval(timer); + }; + }, [gameStarted, isWin]); + + const startGame = () => { + setGameStarted(true); + setMoves(0); + setTime(0); + setIsWin(false); + }; + + const handleWin = () => { + setIsWin(true); + setGameStarted(false); + }; + + const incrementMoves = () => { + setMoves(prevMoves => prevMoves + 1); + }; + + return ( +
+ + Sliding Puzzle Game + + + + +

Sliding Puzzle Game

+ + {!gameStarted && !isWin && ( +
+

Select Difficulty

+
+ + + +
+ +
+ )} + + {gameStarted && ( + <> + + + + )} + + {isWin && ( +
+

Congratulations! You Won!

+

+ Moves: {moves} | Time: {Math.floor(time / 60)}:{(time % 60).toString().padStart(2, '0')} +

+ +
+ )} + + +
+ ); +} \ No newline at end of file diff --git a/puzzle-game/public/favicon.ico b/puzzle-game/public/favicon.ico new file mode 100644 index 000000000..ea944894a --- /dev/null +++ b/puzzle-game/public/favicon.ico @@ -0,0 +1 @@ +[This is a placeholder for a binary favicon.ico file. In a real project, you would need to replace this with an actual favicon.ico file.] \ No newline at end of file diff --git a/puzzle-game/start-game.bat b/puzzle-game/start-game.bat new file mode 100644 index 000000000..f93cdee8c --- /dev/null +++ b/puzzle-game/start-game.bat @@ -0,0 +1,5 @@ +@echo off +echo Starting Next.js Puzzle Game... +start launcher.html +echo If the game doesn't start automatically, please open launcher.html manually. +pause \ No newline at end of file diff --git a/puzzle-game/styles/globals.css b/puzzle-game/styles/globals.css new file mode 100644 index 000000000..c067c6327 --- /dev/null +++ b/puzzle-game/styles/globals.css @@ -0,0 +1,65 @@ +:root { + --primary-color: #0070f3; + --secondary-color: #ff4081; + --background-color: #f5f5f5; + --text-color: #333; + --border-color: #ddd; +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +html, +body { + max-width: 100vw; + overflow-x: hidden; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, + Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; + background-color: var(--background-color); + color: var(--text-color); +} + +a { + color: inherit; + text-decoration: none; +} + +button { + cursor: pointer; + border: none; + background-color: var(--primary-color); + color: white; + padding: 10px 15px; + border-radius: 4px; + font-size: 16px; + transition: background-color 0.3s; +} + +button:hover { + background-color: #0051a8; +} + +.container { + min-height: 100vh; + padding: 20px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.title { + margin-bottom: 20px; + font-size: 2.5rem; + text-align: center; +} + +.subtitle { + margin-bottom: 30px; + font-size: 1.2rem; + text-align: center; + color: #666; +} \ No newline at end of file diff --git a/puzzle-game/tsconfig.json b/puzzle-game/tsconfig.json new file mode 100644 index 000000000..2b808f366 --- /dev/null +++ b/puzzle-game/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} \ No newline at end of file