diff --git a/completesolution/js_vite_react_game/.gitignore b/completesolution/js_vite_react_game/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/completesolution/js_vite_react_game/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/completesolution/js_vite_react_game/eslint.config.js b/completesolution/js_vite_react_game/eslint.config.js new file mode 100644 index 0000000..ec2b712 --- /dev/null +++ b/completesolution/js_vite_react_game/eslint.config.js @@ -0,0 +1,33 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' + +export default [ + { ignores: ['dist'] }, + { + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...reactHooks.configs.recommended.rules, + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +] diff --git a/completesolution/js_vite_react_game/index.html b/completesolution/js_vite_react_game/index.html new file mode 100644 index 0000000..0c589ec --- /dev/null +++ b/completesolution/js_vite_react_game/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/completesolution/js_vite_react_game/package.json b/completesolution/js_vite_react_game/package.json new file mode 100644 index 0000000..a609fcc --- /dev/null +++ b/completesolution/js_vite_react_game/package.json @@ -0,0 +1,27 @@ +{ + "name": "reactgame", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "vite": "^6.2.0" + } +} diff --git a/completesolution/js_vite_react_game/plugins/asciiArtToCssPlugin.js b/completesolution/js_vite_react_game/plugins/asciiArtToCssPlugin.js new file mode 100644 index 0000000..e69b116 --- /dev/null +++ b/completesolution/js_vite_react_game/plugins/asciiArtToCssPlugin.js @@ -0,0 +1,46 @@ +import fs from 'fs'; +import path from 'path'; +import { generateAsciiArtCss } from './generateAsciiArtCss.js'; + +export default function asciiArtToCssPlugin() { + return { + name: 'ascii-art-to-css', + enforce: 'pre', // Ensure this plugin runs before other plugins + handleHotUpdate({ file, server }) { + if (file.endsWith('.atxt')) { + const content = fs.readFileSync(file, 'utf-8'); + if (content.startsWith('#ascii-art')) { + // Trigger a full page reload when a valid ASCII art file changes + server.ws.send({ type: 'full-reload' }); + } + } + }, + transform(code, id) { + if (id.endsWith('.atxt')) { + // Extract the basename of the file (e.g., "enemy" from "enemy.atxt") + const className = path.basename(id, '.atxt'); + + // Generate CSS using the utility function + const cssContent = generateAsciiArtCss(code, className); + + console.log(cssContent); + + // During development, inject CSS dynamically + if (process.env.NODE_ENV === 'development') { + return ` + const style = document.createElement('style'); + style.textContent = \`${cssContent}\`; + document.head.appendChild(style); + `; + } + + // During production, write CSS to disk + const cssFilePath = id.replace(/\.atxt$/, '.css'); + fs.writeFileSync(cssFilePath, cssContent, 'utf-8'); + + // Return nothing for production builds + return ''; + } + }, + }; +} \ No newline at end of file diff --git a/completesolution/js_vite_react_game/plugins/asciiArtToSvgPlugin.js b/completesolution/js_vite_react_game/plugins/asciiArtToSvgPlugin.js new file mode 100644 index 0000000..932c3d1 --- /dev/null +++ b/completesolution/js_vite_react_game/plugins/asciiArtToSvgPlugin.js @@ -0,0 +1,18 @@ +import fs from 'fs'; +import path from 'path'; +import { generateAsciiArtSvg } from './generateAsciiArtSvg.js'; + +export default function asciiArtToSvgPlugin() { + return { + name: 'ascii-art-to-svg', + enforce: 'pre', // Ensure this plugin runs before other plugins + transform(code, id) { + if (id.endsWith('.atxt')) { + // Generate the SVG string from the .atxt content + const svgContent = generateAsciiArtSvg(code, 'ascii-art-svg'); + // Return the SVG string as a JavaScript module + return `export default ${JSON.stringify(svgContent)};`; + } + }, + }; +} \ No newline at end of file diff --git a/completesolution/js_vite_react_game/plugins/generateAsciiArtCss.js b/completesolution/js_vite_react_game/plugins/generateAsciiArtCss.js new file mode 100644 index 0000000..69a70b2 --- /dev/null +++ b/completesolution/js_vite_react_game/plugins/generateAsciiArtCss.js @@ -0,0 +1,62 @@ +/** + * Generates CSS from an .atxt file's content. + * @param {string} atxtContent - The full content of the .atxt file, including the header. + * @param {string} className - The class name to use in the generated CSS. + * @returns {string} - The generated CSS string. + * @throws {Error} - Throws an error if the size in the header is invalid. + */ +export function generateAsciiArtCss(atxtContent, className) { + // Split the content into lines + const lines = atxtContent.trim().split('\n'); + + // Parse the header + const header = lines[0]; + if (!header.startsWith('#ascii-art')) { + throw new Error('Invalid .atxt file: Missing #ascii-art header.'); + } + + // Extract the size from the header (e.g., "10x10") + const sizeMatch = header.match(/#ascii-art (\d+)x(\d+)/); + if (!sizeMatch) { + throw new Error('Invalid .atxt file: Missing or invalid size in header.'); + } + const [_, width, height] = sizeMatch.map(Number); + + // Extract the ASCII art content + const asciiArt = lines.slice(1); + + // Fill missing rows with blank lines to match the specified height + const filledAsciiArt = Array.from({ length: height }, (_, y) => { + return asciiArt[y] ? asciiArt[y].padEnd(width, ' ') : ' '.repeat(width); + }); + + // Validate the dimensions of the ASCII art + for (const line of filledAsciiArt) { + if (line.length !== width) { + throw new Error(`Invalid .atxt file: Expected each row to have ${width} columns, but got "${line}".`); + } + } + + // Convert ASCII art to CSS box-shadow styles + const boxShadow = filledAsciiArt + .flatMap((line, y) => + line.split('').map((char, x) => { + if (char === ' ') return null; // Skip empty spaces + const color = char === '#' ? 'black' : 'gray'; // Define colors based on characters + return `${x}px ${y}px 0 0 ${color}`; + }) + ) + .filter(Boolean) // Remove null values + .join(',\n '); // Format for better readability + + // Generate the CSS content + return ` + .${className} { + width: 1px; + height: 1px; + box-shadow: ${boxShadow}; + transform: scale(10); /* Scale up the image */ + transform-origin: top left; /* Important for scaling */ + } + `; +} \ No newline at end of file diff --git a/completesolution/js_vite_react_game/plugins/generateAsciiArtSvg.js b/completesolution/js_vite_react_game/plugins/generateAsciiArtSvg.js new file mode 100644 index 0000000..d82306e --- /dev/null +++ b/completesolution/js_vite_react_game/plugins/generateAsciiArtSvg.js @@ -0,0 +1,57 @@ +/** + * Generates an SVG string from an .atxt file's content. + * @param {string} atxtContent - The full content of the .atxt file, including the header. + * @param {string} className - The class name to use in the generated SVG. + * @returns {string} - The generated SVG string. + * @throws {Error} - Throws an error if the size in the header is invalid. + */ +export function generateAsciiArtSvg(atxtContent, className) { + // Split the content into lines + const lines = atxtContent.trim().split('\n'); + + // Parse the header + const header = lines[0]; + if (!header.startsWith('#ascii-art')) { + throw new Error('Invalid .atxt file: Missing #ascii-art header.'); + } + + // Extract the size from the header (e.g., "10x10") + const sizeMatch = header.match(/#ascii-art (\d+)x(\d+)/); + if (!sizeMatch) { + throw new Error('Invalid .atxt file: Missing or invalid size in header.'); + } + const [_, width, height] = sizeMatch.map(Number); + + // Extract the ASCII art content + const asciiArt = lines.slice(1); + + // Fill missing rows with blank lines to match the specified height + const filledAsciiArt = Array.from({ length: height }, (_, y) => { + return asciiArt[y] ? asciiArt[y].padEnd(width, ' ') : ' '.repeat(width); + }); + + // Generate the SVG `` elements + const rects = filledAsciiArt + .flatMap((line, y) => + line.split('').map((char, x) => { + if (char === ' ') return null; // Skip empty spaces + const color = char === '#' ? 'black' : 'gray'; // Define colors based on characters + return ``; + }) + ) + .filter(Boolean) // Remove null values + .join('\n '); // Format for better readability + + // Generate the full SVG content + return ` + + ${rects} + + `; +} \ No newline at end of file diff --git a/completesolution/js_vite_react_game/public/vite.svg b/completesolution/js_vite_react_game/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/completesolution/js_vite_react_game/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/completesolution/js_vite_react_game/src/App.css b/completesolution/js_vite_react_game/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/completesolution/js_vite_react_game/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/completesolution/js_vite_react_game/src/App.jsx b/completesolution/js_vite_react_game/src/App.jsx new file mode 100644 index 0000000..2bd8889 --- /dev/null +++ b/completesolution/js_vite_react_game/src/App.jsx @@ -0,0 +1,131 @@ +import React, { useState } from 'react'; +import './App.css'; +import enemy from './ascii-art/enemy.atxt'; +import ship from './ascii-art/ship.atxt'; +import ammo from './ascii-art/ammo.atxt'; + +function App() { + // State to hold an array of enemies with their coordinates + const [enemies, setEnemies] = useState([ + { id: 1, x: 10, y: 10 }, + { id: 2, x: 30, y: 10 }, + { id: 3, x: 50, y: 10 }, + ]); + + // State to hold the ship's coordinates + const [shipPosition, setShipPosition] = useState({ x: 30, y: 64 }); + + // State to hold ammo positions + const [ammoPositions, setAmmoPositions] = useState([ + { id: 1, x: 10, y: 50 } + ]); + + return ( + <> +
+ {/* Render enemies dynamically */} + {enemies.map((enemyObj) => ( +
+ ))} + + {/* Render ammo dynamically */} + {ammoPositions.map((ammoObj) => ( +
+ ))} + + {/* Render the ship */} +
+
+ + + ); +} + +export default App; diff --git a/completesolution/js_vite_react_game/src/ascii-art/ammo.atxt b/completesolution/js_vite_react_game/src/ascii-art/ammo.atxt new file mode 100644 index 0000000..0b83cda --- /dev/null +++ b/completesolution/js_vite_react_game/src/ascii-art/ammo.atxt @@ -0,0 +1,4 @@ +#ascii-art 1x3 +x +x +x \ No newline at end of file diff --git a/completesolution/js_vite_react_game/src/ascii-art/enemy.atxt b/completesolution/js_vite_react_game/src/ascii-art/enemy.atxt new file mode 100644 index 0000000..d952e89 --- /dev/null +++ b/completesolution/js_vite_react_game/src/ascii-art/enemy.atxt @@ -0,0 +1,11 @@ +#ascii-art 10x10 + xxxxxx + xxxxxxxx +x x +x xx xx x + xx xx + xx xx +xxxxxxxxxx + xx xx + x x + x x \ No newline at end of file diff --git a/completesolution/js_vite_react_game/src/ascii-art/ship.atxt b/completesolution/js_vite_react_game/src/ascii-art/ship.atxt new file mode 100644 index 0000000..03f285b --- /dev/null +++ b/completesolution/js_vite_react_game/src/ascii-art/ship.atxt @@ -0,0 +1,11 @@ +#ascii-art 10x10 + xx + xxxx + xxxxxx + xxxxxxxx + x xxxx x +xx xxxx xx +xx x x xx + x xxxx x + xxxxxx + xx \ No newline at end of file diff --git a/completesolution/js_vite_react_game/src/assets/react.svg b/completesolution/js_vite_react_game/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/completesolution/js_vite_react_game/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/completesolution/js_vite_react_game/src/index.css b/completesolution/js_vite_react_game/src/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/completesolution/js_vite_react_game/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/completesolution/js_vite_react_game/src/main.jsx b/completesolution/js_vite_react_game/src/main.jsx new file mode 100644 index 0000000..b9a1a6d --- /dev/null +++ b/completesolution/js_vite_react_game/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/completesolution/js_vite_react_game/vite.config.js b/completesolution/js_vite_react_game/vite.config.js new file mode 100644 index 0000000..2f37943 --- /dev/null +++ b/completesolution/js_vite_react_game/vite.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import asciiArtToSvgPlugin from './plugins/asciiArtToSvgPlugin.js' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(), asciiArtToSvgPlugin()], +}) diff --git a/exercisefiles/js_vite_react_game/README.md b/exercisefiles/js_vite_react_game/README.md new file mode 100644 index 0000000..f1f718b --- /dev/null +++ b/exercisefiles/js_vite_react_game/README.md @@ -0,0 +1,194 @@ +# Learning GitHub Copilot: Build a React Game with ASCII Art + +This guide will help you learn how to use GitHub Copilot to create a project similar to this ASCII-art-based React game. Follow the steps below to build your own project while leveraging Copilot's AI-powered suggestions. + +--- + +## Prerequisites + +Before starting, ensure you have the following installed: + +1. **Node.js** (v16 or higher) +2. **npm** (comes with Node.js) +3. **Visual Studio Code** with the **GitHub Copilot extension** installed. + +--- + +## Step-by-Step Guide + +### 1. **Set Up Your Project** + +The `completesolution` folder in this project contains a partially implemented game you can use as an example. You can either build on it to add advanced features or start your own project using the steps below. + +1. Open Visual Studio Code and create a new folder for your project. +2. Open the terminal in VS Code and run the following commands to initialize a Vite + React project: + + ```bash + npm create vite@latest reactgame --template react + cd reactgame + npm install + ``` + +3. Install the necessary dependencies for ESLint and React: + + ```bash + npm install eslint @eslint/js eslint-plugin-react-hooks eslint-plugin-react-refresh globals --save-dev + ``` + +4. Open the `package.json` file and configure the scripts section to include: + + ```json + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + } + ``` + +--- + +### 2. **Create ASCII Art Files** + +1. Create a folder named `src/ascii-art` in your project. +2. Add `.atxt` files for your game assets (e.g., `ship.atxt`, `enemy.atxt`, `ammo.atxt`). Use ASCII art to define your game objects. For example, you can create simple designs like spaceships, enemies, or projectiles. + + ```atxt + #ascii-art 10x10 + xx + xxxx + xxxxxx + xxxxxxxx + x xxxx x + xx xxxx xx + xx x x xx + x xxxx x + xxxxxx + xx + ``` + +3. Use Copilot to help you generate ASCII art by typing comments like `// ASCII art for a spaceship` and letting Copilot suggest designs. + +--- + +### 3. **Write a Plugin to Convert ASCII Art** + +The reason we are writing a plugin to generate SVGs from our custom text files (`.atxt`) is to enable automatic hot-reloading of changes on the webpage without needing to run conversion commands or manually refresh the page. + +1. Create a `plugins` folder in your project. +2. Add a file named `asciiArtToSvgPlugin.js` to convert `.atxt` files into SVGs. Use Copilot to scaffold the plugin by typing comments like `// Plugin to convert ASCII art to SVG`. + + Example: + + ```js + import fs from 'fs'; + import path from 'path'; + + export default function asciiArtToSvgPlugin() { + return { + name: 'ascii-art-to-svg', + transform(code, id) { + if (id.endsWith('.atxt')) { + // Copilot can help generate the SVG conversion logic here + // Remember to mention about your ascii file and there is header also at the first line etc + } + }, + }; + } + ``` + +3. Add the plugin to your `vite.config.js` file: + + ```js + import asciiArtToSvgPlugin from './plugins/asciiArtToSvgPlugin.js'; + + export default defineConfig({ + plugins: [react(), asciiArtToSvgPlugin()], + }); + ``` + +--- + +### 4. **Build the Game UI** + +1. Create a `src/App.jsx` file and use Copilot to scaffold the React components for your game. For example: + + ```jsx + import React, { useState } from 'react'; + + function App() { + const [enemies, setEnemies] = useState([]); + const [shipPosition, setShipPosition] = useState({ x: 0, y: 0 }); + + return ( +
+ {/* Copilot can help you dynamically render game elements */} +
+ ); + } + + export default App; + ``` + +2. Use Copilot to generate logic for rendering ASCII art dynamically, handling game state, and detecting collisions. + +--- + +### 5. **Style the Game** + +1. Create a `src/index.css` file and use Copilot to help you write CSS for your game. For example: + + ```css + body { + margin: 0; + display: flex; + justify-content: center; + align-items: center; + background-color: #242424; + } + ``` + +2. Use Copilot to suggest styles for buttons, animations, and responsive layouts. + +--- + +### 6. **Run and Test Your Game** + +1. Start the development server: + + ```bash + npm run dev + ``` + +2. Open the game in your browser and test the functionality. Use Copilot to debug and improve your code as needed. + +--- + +### 7. **Iterate and Expand** + +1. Add new features to your game, such as: + + - Player controls for moving the ship. + - Shooting mechanics. + - Enemy movement and spawning. + +2. Use Copilot to help you write the logic for these features. + +--- + +### 8. **Share Your Project** + +1. Push your project to GitHub. +2. Write a README file (like this one) to document your project and guide others. + +--- + +## Tips for Using Copilot Effectively + +- **Write clear comments**: Describe what you want to achieve, and Copilot will suggest relevant code. +- **Iterate on suggestions**: If Copilot's suggestion isn't perfect, refine it or try rephrasing your comment. +- **Learn from the code**: Use Copilot's suggestions as a learning tool to understand new concepts and patterns. + +--- + +By following this guide, you'll not only build a fun React game but also learn how to use GitHub Copilot to accelerate your development process. Happy coding!