Commit be00b7bf authored by Smirnov Oleg's avatar Smirnov Oleg

inital

parent 4a0c0759
This diff is collapsed.
......@@ -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"
}
}
This diff is collapsed.
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;
This diff is collapsed.
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