Commit be00b7bf authored by Smirnov Oleg's avatar Smirnov Oleg

inital

parent 4a0c0759
......@@ -10,7 +10,9 @@
"dependencies": {
"next": "15.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
......@@ -831,6 +833,11 @@
"integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==",
"dev": true
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
......@@ -844,6 +851,19 @@
"tslib": "^2.8.0"
}
},
"node_modules/@types/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
},
"node_modules/@types/cors": {
"version": "2.8.17",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
"integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
......@@ -866,7 +886,6 @@
"version": "20.17.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz",
"integrity": "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==",
"dev": true,
"dependencies": {
"undici-types": "~6.19.2"
}
......@@ -1113,6 +1132,18 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
......@@ -1412,6 +1443,14 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
......@@ -1654,6 +1693,26 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
......@@ -1859,6 +1918,78 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/engine.io": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
"integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==",
"dependencies": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/engine.io-client": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz",
"integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/enhanced-resolve": {
"version": "5.17.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
......@@ -3592,6 +3723,25 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
......@@ -3625,8 +3775,7 @@
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/mz": {
"version": "2.7.0",
......@@ -3662,6 +3811,14 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/next": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/next/-/next-15.1.0.tgz",
......@@ -3755,7 +3912,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
......@@ -4604,6 +4760,122 @@
"is-arrayish": "^0.3.1"
}
},
"node_modules/socket.io": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.3.2",
"engine.io": "~6.6.0",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
"dependencies": {
"debug": "~4.3.4",
"ws": "~8.17.1"
}
},
"node_modules/socket.io-adapter/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
......@@ -5176,8 +5448,7 @@
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
},
"node_modules/uri-js": {
"version": "4.4.1",
......@@ -5194,6 +5465,14 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
......@@ -5389,6 +5668,34 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/yaml": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz",
......
......@@ -9,19 +9,21 @@
"lint": "next lint"
},
"dependencies": {
"next": "15.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.1.0"
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"typescript": "^5",
"@eslint/eslintrc": "^3",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^9",
"eslint-config-next": "15.1.0",
"@eslint/eslintrc": "^3"
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
import Image from "next/image";
'use client';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { io, Socket } from 'socket.io-client';
import GameBoard from '../components/GameBoard';
import PlayerInfo from '../components/PlayerInfo';
import GameInvite from '../components/GameInvite';
import { GameState, Player, GameMove } from '../types/game';
let socket: Socket;
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
src/app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
const searchParams = useSearchParams();
const [roomId, setRoomId] = useState<string>('');
const [showInvite, setShowInvite] = useState(false);
const [showTimeoutMessage, setShowTimeoutMessage] = useState<string | null>(null);
const [gameState, setGameState] = useState<GameState>({
currentPlayer: 'X',
cells: {},
winner: null,
isYourTurn: false,
status: 'waiting',
players: {},
turnTimeLimit: 20000 // 20 секунд
});
const [turnTimeLeft, setTurnTimeLeft] = useState<number>(20000);
useEffect(() => {
const roomFromUrl = searchParams.get('room');
if (!socket) {
socket = io({
path: '/api/socket',
});
socket.on('connect', () => {
console.log('Connected to server');
});
socket.on('gameState', (state: GameState) => {
setGameState(state);
if (state.turnStartTime) {
const timeLeft = state.turnTimeLimit - (Date.now() - state.turnStartTime);
setTurnTimeLeft(Math.max(0, timeLeft));
}
});
socket.on('turnTimeout', ({ player }) => {
setShowTimeoutMessage(`Время хода ${player} истекло!`);
setTimeout(() => setShowTimeoutMessage(null), 3000);
});
socket.on('playerDisconnected', () => {
alert('Противник отключился');
window.location.href = '/';
});
}
if (roomFromUrl) {
socket.emit('joinGame', { roomId: roomFromUrl });
setRoomId(roomFromUrl);
}
return () => {
if (socket) {
socket.disconnect();
}
};
}, [searchParams]);
useEffect(() => {
let timer: NodeJS.Timeout;
if (gameState.isYourTurn && gameState.turnStartTime) {
timer = setInterval(() => {
const timeLeft = gameState.turnTimeLimit - (Date.now() - gameState.turnStartTime);
setTurnTimeLeft(Math.max(0, timeLeft));
if (timeLeft <= 0) {
socket.emit('turnTimeout', { roomId });
clearInterval(timer);
}
}, 100);
}
return () => clearInterval(timer);
}, [gameState.isYourTurn, gameState.turnStartTime, gameState.turnTimeLimit, roomId]);
const handleCreateGame = () => {
socket.emit('createGame');
socket.once('gameCreated', ({ roomId, gameState }: { roomId: string, gameState: GameState }) => {
setRoomId(roomId);
setGameState(gameState);
setShowInvite(true);
});
};
const handleNewGame = () => {
socket.emit('readyForNewGame', { roomId });
};
const handleMove = (x: number, y: number) => {
if (gameState.isYourTurn && !gameState.winner && turnTimeLeft > 0) {
socket.emit('move', {
x,
y,
player: gameState.playerSymbol,
roomId
});
}
};
const currentPlayer = gameState.players.attacker?.id === socket?.id
? gameState.players.attacker
: gameState.players.defender;
const opponent = gameState.players.attacker?.id === socket?.id
? gameState.players.defender
: gameState.players.attacker;
if (!roomId) {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
<button
onClick={handleCreateGame}
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg text-xl transition-colors"
>
Создать игру
</button>
</div>
);
}
return (
<div className="relative min-h-screen bg-gray-900 text-white">
{showInvite && <GameInvite roomId={roomId} />}
{showTimeoutMessage && (
<div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2
bg-red-600 text-white px-6 py-3 rounded-lg text-xl z-50">
{showTimeoutMessage}
</div>
)}
{currentPlayer && (
<div className="fixed top-4 left-4 bg-gray-800 rounded p-2">
<PlayerInfo
nickname={`${currentPlayer.nickname} (${currentPlayer.score})`}
isCurrentTurn={gameState.isYourTurn}
isAttacker={currentPlayer.isAttacker}
position="top"
/>
</div>
)}
{opponent && (
<div className="fixed bottom-4 left-4 bg-gray-800 rounded p-2">
<PlayerInfo
nickname={`${opponent.nickname} (${opponent.score})`}
isCurrentTurn={!gameState.isYourTurn}
isAttacker={opponent.isAttacker}
position="bottom"
/>
</div>
)}
<div className="pt-20 pb-20">
{gameState.status === 'waiting' ? (
<div className="text-center text-xl">
Waiting for opponent to join...
</div>
) : (
<>
<div className="max-w-screen-sm mx-auto">
<GameBoard
cells={gameState.cells}
onCellClick={handleMove}
isYourTurn={gameState.isYourTurn}
playerSymbol={gameState.playerSymbol}
lastMove={gameState.lastMove}
turnTimeLeft={gameState.isYourTurn ? turnTimeLeft : undefined}
/>
</div>
{gameState.winner && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-gray-800 p-8 rounded-lg text-center">
<h2 className="text-3xl font-bold mb-4">
{(gameState.winner === 'X' && currentPlayer?.isAttacker) ||
(gameState.winner === 'O' && !currentPlayer?.isAttacker)
? 'Вы победили!'
: 'Вы проиграли!'}
</h2>
<div className="text-xl mb-4">
Счет: {currentPlayer?.nickname} ({currentPlayer?.score}) - {opponent?.nickname} ({opponent?.score})
</div>
<button
onClick={handleNewGame}
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
>
Играть снова
</button>
</div>
</div>
)}
</>
)}
</div>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
</div>
);
);
}
import React, { useState, useEffect, useRef } from 'react';
import { CellValue } from '../types/game';
import ServerIcon from './ServerIcon';
interface GameBoardProps {
cells: { [key: string]: CellValue };
onCellClick: (x: number, y: number) => void;
isYourTurn: boolean;
playerSymbol?: 'X' | 'O';
lastMove?: { x: number; y: number; player: 'X' | 'O' };
turnTimeLeft?: number;
}
const GameBoard: React.FC<GameBoardProps> = ({
cells = {},
onCellClick,
isYourTurn,
playerSymbol,
lastMove,
turnTimeLeft
}) => {
const [viewportPosition, setViewportPosition] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
const containerRef = useRef<HTMLDivElement>(null);
const cellSize = 60;
const viewportSize = 800;
useEffect(() => {
if (containerRef.current) {
setViewportPosition({
x: viewportSize / 2,
y: viewportSize / 2
});
}
}, []);
const getCellValue = (x: number, y: number): CellValue => {
const key = `${x},${y}`;
return cells[key] || null;
};
const handleCellClick = (x: number, y: number) => {
if (!isDragging) {
const value = getCellValue(x, y);
if (!value && isYourTurn) {
onCellClick(x, y);
}
}
};
const handleMouseDown = (e: React.MouseEvent) => {
setIsDragging(true);
setDragStart({
x: e.clientX - viewportPosition.x,
y: e.clientY - viewportPosition.y
});
};
const handleMouseMove = (e: React.MouseEvent) => {
if (isDragging) {
setViewportPosition({
x: e.clientX - dragStart.x,
y: e.clientY - dragStart.y
});
}
};
const handleMouseUp = () => {
setIsDragging(false);
};
const getVisibleCells = () => {
if (!containerRef.current) return [];
const container = containerRef.current;
const visibleCells = [];
const startX = Math.floor((-viewportPosition.x) / cellSize) - 2;
const startY = Math.floor((-viewportPosition.y) / cellSize) - 2;
const cols = Math.ceil(container.clientWidth / cellSize) + 4;
const rows = Math.ceil(container.clientHeight / cellSize) + 4;
for (let y = startY; y < startY + rows; y++) {
for (let x = startX; x < startX + cols; x++) {
visibleCells.push({ x, y });
}
}
return visibleCells;
};
const getServerIconState = (value: CellValue, isPreview: boolean = false): 'empty' | 'attacker' | 'defender' => {
if (!value && !isPreview) return 'empty';
if (isPreview) return playerSymbol === 'X' ? 'attacker' : 'defender';
return value === 'X' ? 'attacker' : 'defender';
};
const renderCell = ({ x, y }: { x: number; y: number }) => {
const value = getCellValue(x, y);
const key = `${x},${y}`;
const isHoverable = !value && isYourTurn;
return (
<div
key={key}
onClick={() => handleCellClick(x, y)}
className={`absolute flex items-center justify-center w-[58px] h-[58px] border border-gray-700 bg-gray-800
transition-all duration-200 ${isHoverable ? 'hover:bg-gray-700 cursor-pointer' : ''}`}
style={{
left: x * cellSize,
top: y * cellSize,
transform: `translate(1px, 1px)`,
}}
>
{value && (
<div className="transform transition-transform duration-200 scale-100">
<ServerIcon state={getServerIconState(value)} />
</div>
)}
{isHoverable && !value && (
<div className="absolute opacity-20">
<ServerIcon state={getServerIconState(null, true)} />
</div>
)}
</div>
);
};
const moveToLastMove = () => {
if (lastMove) {
setViewportPosition({
x: viewportSize / 2 - lastMove.x * cellSize,
y: viewportSize / 2 - lastMove.y * cellSize
});
}
};
return (
<div className="relative">
<div
ref={containerRef}
className={`relative w-[800px] h-[800px] bg-gray-900 overflow-hidden mx-auto
${isDragging ? 'cursor-grabbing' : 'cursor-grab'}`}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
<div
style={{
transform: `translate(${viewportPosition.x}px, ${viewportPosition.y}px)`,
}}
className="absolute transition-transform duration-75"
>
{getVisibleCells().map(renderCell)}
</div>
</div>
<div className="absolute top-4 right-4 flex flex-col gap-2">
{lastMove && (
<button
onClick={moveToLastMove}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded transition-colors"
>
Показать последний ход
</button>
)}
{turnTimeLeft !== undefined && (
<div className={`px-4 py-2 rounded ${
turnTimeLeft < 5000 ? 'bg-red-600' : 'bg-gray-700'
}`}>
Осталось: {Math.ceil(turnTimeLeft / 1000)}с
</div>
)}
</div>
</div>
);
};
export default GameBoard;
import React from 'react';
interface GameInviteProps {
roomId: string;
}
const GameInvite: React.FC<GameInviteProps> = ({ roomId }) => {
const gameUrl = typeof window !== 'undefined' ? `${window.location.origin}?room=${roomId}` : '';
const shareText = `Присоединяйся ко мне в игре!`;
const shareLinks = {
telegram: `https://t.me/share/url?url=${encodeURIComponent(gameUrl)}&text=${encodeURIComponent(shareText)}`,
whatsapp: `https://wa.me/?text=${encodeURIComponent(shareText + '\n' + gameUrl)}`,
vk: `https://vk.com/share.php?url=${encodeURIComponent(gameUrl)}&title=${encodeURIComponent(shareText)}`
};
const copyToClipboard = () => {
navigator.clipboard.writeText(gameUrl);
};
return (
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-gray-800 p-8 rounded-lg shadow-xl text-center max-w-lg w-full mx-4">
<h2 className="text-2xl font-bold mb-4">Ваша комната создана!</h2>
<div className="bg-gray-700 p-4 rounded mb-4 break-all">
<p className="text-sm font-mono select-all">{gameUrl}</p>
</div>
<button
onClick={copyToClipboard}
className="mb-4 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded transition-colors w-full"
>
Копировать ссылку
</button>
<div className="flex justify-center space-x-4">
<a
href={shareLinks.telegram}
target="_blank"
rel="noopener noreferrer"
className="p-2 bg-blue-500 hover:bg-blue-600 rounded transition-colors flex-1"
>
Telegram
</a>
<a
href={shareLinks.whatsapp}
target="_blank"
rel="noopener noreferrer"
className="p-2 bg-green-500 hover:bg-green-600 rounded transition-colors flex-1"
>
WhatsApp
</a>
<a
href={shareLinks.vk}
target="_blank"
rel="noopener noreferrer"
className="p-2 bg-blue-600 hover:bg-blue-700 rounded transition-colors flex-1"
>
VK
</a>
</div>
</div>
</div>
);
};
export default GameInvite;
import React from 'react';
interface PlayerInfoProps {
nickname: string;
isCurrentTurn: boolean;
isAttacker: boolean;
position: 'top' | 'bottom';
}
const PlayerInfo: React.FC<PlayerInfoProps> = ({ nickname, isCurrentTurn, isAttacker, position }) => {
return (
<div className={`fixed ${position === 'top' ? 'top-4' : 'bottom-4'} left-4 flex items-center gap-4 bg-gray-800 rounded-lg p-4 text-white`}>
<div className={`w-12 h-12 rounded-full flex items-center justify-center ${
isCurrentTurn ? (isAttacker ? 'bg-red-500' : 'bg-green-500') : 'bg-gray-600'
}`}>
{nickname.charAt(0).toUpperCase()}
</div>
<div>
<div className="font-medium">{nickname}</div>
<div className={`text-sm ${isCurrentTurn ? 'text-yellow-400' : 'text-gray-400'}`}>
{isCurrentTurn ? 'Your turn' : 'Waiting...'}
</div>
</div>
</div>
);
};
export default PlayerInfo;
import React from 'react';
interface ServerIconProps {
state: 'empty' | 'attacker' | 'defender';
}
const ServerIcon: React.FC<ServerIconProps> = ({ state }) => {
const color = state === 'empty' ? '#4B5563' :
state === 'attacker' ? '#EF4444' : '#10B981';
return (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 9V15M19 15V19C19 20.1046 18.1046 21 17 21H7C5.89543 21 5 20.1046 5 19V5C5 3.89543 5.89543 3 7 3H17C18.1046 3 19 3.89543 19 5V9M19 15H5M19 9H5M9 7H7M9 13H7M9 19H7"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<circle cx="17" cy="7" r="1" fill={color} />
<circle cx="17" cy="13" r="1" fill={color} />
<circle cx="17" cy="19" r="1" fill={color} />
</svg>
);
};
export default ServerIcon;
import { Server } from 'socket.io';
import { Server as NetServer } from 'http';
import { NextApiRequest } from 'next';
import { NextApiResponseWithSocket } from '../../types/next';
import { GameState, GameMove } from '../../types/game';
const games = new Map<string, GameState>();
export const config = {
api: {
bodyParser: false,
},
};
const generateRoomId = () => Math.random().toString(36).substring(2, 8);
const createNewGame = (playerId: string): [string, GameState] => {
const roomId = generateRoomId();
const gameState: GameState = {
currentPlayer: 'X',
cells: {},
winner: null,
isYourTurn: false,
status: 'waiting',
turnTimeLimit: 20000,
turnStartTime: null,
players: {
attacker: {
id: playerId,
nickname: 'Heker',
isAttacker: true,
score: 0
}
},
readyForNewGame: {}
};
games.set(roomId, gameState);
return [roomId, gameState];
};
const joinGame = (roomId: string, playerId: string): GameState | null => {
const game = games.get(roomId);
if (!game || game.status !== 'waiting') return null;
game.players.defender = {
id: playerId,
nickname: 'Beluga',
isAttacker: false,
score: 0
};
game.status = 'playing';
game.turnStartTime = Date.now();
return game;
};
const startNewRound = (game: GameState, firstReadyPlayerId: string): GameState => {
// Определяем, кто будет атакующим в новой игре (тот, кто первый нажал "Играть снова")
const oldAttacker = game.players.attacker!;
const oldDefender = game.players.defender!;
// Определяем, кто первый нажал кнопку
const firstPlayer = firstReadyPlayerId === oldAttacker.id ? oldAttacker : oldDefender;
const secondPlayer = firstReadyPlayerId === oldAttacker.id ? oldDefender : oldAttacker;
// Сбрасываем состояние игры
const newGame: GameState = {
...game,
cells: {},
currentPlayer: 'X',
winner: null,
status: 'playing',
lastMove: null,
turnStartTime: Date.now(),
readyForNewGame: {},
// Первый нажавший становится атакующим
players: {
attacker: {
...firstPlayer,
isAttacker: true,
nickname: 'Heker'
},
defender: {
...secondPlayer,
isAttacker: false,
nickname: 'Beluga'
}
}
};
return newGame;
};
const checkWinner = (cells: { [key: string]: string }, lastMove: GameMove): string | null => {
const directions = [
[0, 1], // horizontal
[1, 0], // vertical
[1, 1], // diagonal
[1, -1], // other diagonal
];
const { x, y, player } = lastMove;
for (const [dx, dy] of directions) {
let count = 1;
// Check in positive direction
for (let i = 1; i < 5; i++) {
const key = `${x + dx * i},${y + dy * i}`;
if (cells[key] !== player) break;
count++;
}
// Check in negative direction
for (let i = 1; i < 5; i++) {
const key = `${x - dx * i},${y - dy * i}`;
if (cells[key] !== player) break;
count++;
}
if (count >= 5) return player;
}
return null;
};
const handler = async (req: NextApiRequest, res: NextApiResponseWithSocket) => {
if (!res.socket.server.io) {
const httpServer: NetServer = res.socket.server as any;
const io = new Server(httpServer, {
path: '/api/socket',
});
io.on('connection', (socket) => {
console.log('Client connected:', socket.id);
socket.on('createGame', () => {
const [roomId, gameState] = createNewGame(socket.id);
socket.join(roomId);
socket.emit('gameCreated', { roomId, gameState });
});
socket.on('joinGame', ({ roomId }) => {
const gameState = joinGame(roomId, socket.id);
if (gameState) {
socket.join(roomId);
// Отправляем состояние атакующему игроку
io.to(gameState.players.attacker!.id).emit('gameState', {
...gameState,
playerSymbol: 'X',
isYourTurn: true
});
// Отправляем состояние защищающемуся игроку
socket.emit('gameState', {
...gameState,
playerSymbol: 'O',
isYourTurn: false
});
} else {
socket.emit('error', 'Game not found or already started');
}
});
socket.on('readyForNewGame', ({ roomId }) => {
const game = games.get(roomId);
if (!game) return;
// Создаем новую игру
const newGame: GameState = {
...game,
cells: {},
currentPlayer: 'X',
winner: null,
status: 'playing',
lastMove: null,
turnStartTime: Date.now()
};
// Нажавший кнопку становится атакующим
const oldAttacker = game.players.attacker!;
const oldDefender = game.players.defender!;
if (socket.id === oldAttacker.id) {
// Атакующий остается атакующим
newGame.players = {
attacker: oldAttacker,
defender: oldDefender
};
} else {
// Защищающийся становится атакующим
newGame.players = {
attacker: {
...oldDefender,
isAttacker: true,
nickname: 'Heker'
},
defender: {
...oldAttacker,
isAttacker: false,
nickname: 'Beluga'
}
};
}
games.set(roomId, newGame);
// Отправляем обновленное состояние обоим игрокам
io.to(newGame.players.attacker!.id).emit('gameState', {
...newGame,
playerSymbol: 'X',
isYourTurn: true
});
io.to(newGame.players.defender!.id).emit('gameState', {
...newGame,
playerSymbol: 'O',
isYourTurn: false
});
});
socket.on('move', ({ x, y, player, roomId }) => {
const game = games.get(roomId);
if (!game || game.status !== 'playing') return;
const cellKey = `${x},${y}`;
if (game.cells[cellKey]) return;
game.cells[cellKey] = player;
game.lastMove = { x, y, player };
const winner = checkWinner(game.cells, { x, y, player });
if (winner) {
game.winner = winner;
game.status = 'finished';
// Обновляем счет
if (winner === 'X') {
game.players.attacker!.score = (game.players.attacker!.score || 0) + 1;
} else {
game.players.defender!.score = (game.players.defender!.score || 0) + 1;
}
} else {
game.currentPlayer = game.currentPlayer === 'X' ? 'O' : 'X';
game.turnStartTime = Date.now();
}
// Отправляем обновленное состояние обоим игрокам
io.to(game.players.attacker!.id).emit('gameState', {
...game,
playerSymbol: 'X',
isYourTurn: game.currentPlayer === 'X'
});
io.to(game.players.defender!.id).emit('gameState', {
...game,
playerSymbol: 'O',
isYourTurn: game.currentPlayer === 'O'
});
});
socket.on('turnTimeout', ({ roomId }) => {
const game = games.get(roomId);
if (!game || game.status !== 'playing') return;
// Проверяем, действительно ли время истекло
if (game.turnStartTime && Date.now() - game.turnStartTime > game.turnTimeLimit) {
// Определяем, чей ход был
const timeoutPlayer = game.currentPlayer === 'X' ? 'Heker' : 'Beluga';
// Передаем ход другому игроку
game.currentPlayer = game.currentPlayer === 'X' ? 'O' : 'X';
game.turnStartTime = Date.now();
// Отправляем уведомление всем игрокам
io.to(roomId).emit('turnTimeout', { player: timeoutPlayer });
// Отправляем обновленное состояние обоим игрокам
io.to(game.players.attacker!.id).emit('gameState', {
...game,
playerSymbol: 'X',
isYourTurn: game.currentPlayer === 'X'
});
io.to(game.players.defender!.id).emit('gameState', {
...game,
playerSymbol: 'O',
isYourTurn: game.currentPlayer === 'O'
});
}
});
socket.on('disconnect', () => {
console.log('Client disconnected:', socket.id);
games.forEach((game, roomId) => {
if (game.players.attacker?.id === socket.id || game.players.defender?.id === socket.id) {
io.to(roomId).emit('playerDisconnected', {
message: 'Opponent disconnected'
});
games.delete(roomId);
}
});
});
});
res.socket.server.io = io;
}
res.end();
};
export default handler;
export type CellValue = null | 'X' | 'O';
export type PlayerSymbol = 'X' | 'O';
export interface Cell {
x: number;
y: number;
value: CellValue;
}
export interface Player {
id: string;
nickname: string;
isAttacker: boolean;
score: number;
}
export interface GameRoom {
id: string;
players: {
X?: Player;
O?: Player;
};
currentPlayer: PlayerSymbol;
cells: { [key: string]: CellValue };
winner: CellValue;
status: 'waiting' | 'playing' | 'finished';
}
export interface GameState {
room?: GameRoom;
currentPlayer: 'X' | 'O';
cells: { [key: string]: CellValue };
winner: CellValue;
isYourTurn: boolean;
playerSymbol?: 'X' | 'O';
status: 'waiting' | 'playing' | 'finished';
players: {
attacker?: Player;
defender?: Player;
};
lastMove?: {
x: number;
y: number;
player: 'X' | 'O';
};
turnStartTime?: number;
turnTimeLimit: number; // в миллисекундах
readyForNewGame?: {
[playerId: string]: boolean;
};
}
export interface GameMove {
x: number;
y: number;
player: 'X' | 'O';
roomId: string;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment