mirror of
https://github.com/lockin-bot/react-telegram.git
synced 2026-01-12 15:13:56 +08:00
feat: add tetris...
This commit is contained in:
172
packages/examples/src/calculator-bot.tsx
Normal file
172
packages/examples/src/calculator-bot.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
/// <reference types="@react-telegram/core" />
|
||||
import React, { useState } from "react";
|
||||
import { MtcuteAdapter } from "@react-telegram/mtcute-adapter";
|
||||
|
||||
|
||||
interface CalculatorState {
|
||||
display: string;
|
||||
previousValue: string;
|
||||
operator: string | null;
|
||||
waitingForOperand: boolean;
|
||||
}
|
||||
|
||||
const initialState: CalculatorState = {
|
||||
display: "0",
|
||||
previousValue: "0",
|
||||
operator: null,
|
||||
waitingForOperand: false,
|
||||
};
|
||||
|
||||
const Calculator = () => {
|
||||
const [state, setState] = useState<CalculatorState>(initialState);
|
||||
|
||||
const inputNumber = (num: string) => {
|
||||
if (state.waitingForOperand) {
|
||||
setState({
|
||||
...state,
|
||||
display: num,
|
||||
waitingForOperand: false,
|
||||
});
|
||||
} else {
|
||||
setState({
|
||||
...state,
|
||||
display: state.display === "0" ? num : state.display + num,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const inputDecimal = () => {
|
||||
if (state.waitingForOperand) {
|
||||
setState({
|
||||
...state,
|
||||
display: "0.",
|
||||
waitingForOperand: false,
|
||||
});
|
||||
} else if (state.display.indexOf(".") === -1) {
|
||||
setState({
|
||||
...state,
|
||||
display: state.display + ".",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
setState(initialState);
|
||||
};
|
||||
|
||||
const performOperation = (nextOperator: string | null) => {
|
||||
const inputValue = parseFloat(state.display);
|
||||
|
||||
if (state.previousValue === "0") {
|
||||
setState({
|
||||
...state,
|
||||
previousValue: String(inputValue),
|
||||
operator: nextOperator,
|
||||
waitingForOperand: true,
|
||||
});
|
||||
} else if (state.operator) {
|
||||
const previousValue = parseFloat(state.previousValue);
|
||||
let newValue = previousValue;
|
||||
|
||||
if (state.operator === "+") {
|
||||
newValue = previousValue + inputValue;
|
||||
} else if (state.operator === "-") {
|
||||
newValue = previousValue - inputValue;
|
||||
} else if (state.operator === "*") {
|
||||
newValue = previousValue * inputValue;
|
||||
} else if (state.operator === "/") {
|
||||
newValue = inputValue !== 0 ? previousValue / inputValue : 0;
|
||||
}
|
||||
|
||||
const display = String(newValue);
|
||||
|
||||
setState({
|
||||
display,
|
||||
previousValue: nextOperator ? display : "0",
|
||||
operator: nextOperator,
|
||||
waitingForOperand: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const calculate = () => {
|
||||
performOperation(null);
|
||||
};
|
||||
|
||||
const renderButton = (label: string, onClick: () => void, wide: boolean = false) => (
|
||||
<button onClick={onClick}>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<b>🧮 Calculator</b>
|
||||
<br />
|
||||
<br />
|
||||
<code>{state.display}</code>
|
||||
<br />
|
||||
<br />
|
||||
<row>
|
||||
{renderButton("C", clear)}
|
||||
{renderButton("±", () => setState({ ...state, display: String(-parseFloat(state.display)) }))}
|
||||
{renderButton("%", () => setState({ ...state, display: String(parseFloat(state.display) / 100) }))}
|
||||
{renderButton("÷", () => performOperation("/"))}
|
||||
</row>
|
||||
<row>
|
||||
{renderButton("7", () => inputNumber("7"))}
|
||||
{renderButton("8", () => inputNumber("8"))}
|
||||
{renderButton("9", () => inputNumber("9"))}
|
||||
{renderButton("×", () => performOperation("*"))}
|
||||
</row>
|
||||
<row>
|
||||
{renderButton("4", () => inputNumber("4"))}
|
||||
{renderButton("5", () => inputNumber("5"))}
|
||||
{renderButton("6", () => inputNumber("6"))}
|
||||
{renderButton("−", () => performOperation("-"))}
|
||||
</row>
|
||||
<row>
|
||||
{renderButton("1", () => inputNumber("1"))}
|
||||
{renderButton("2", () => inputNumber("2"))}
|
||||
{renderButton("3", () => inputNumber("3"))}
|
||||
{renderButton("+", () => performOperation("+"))}
|
||||
</row>
|
||||
<row>
|
||||
{renderButton("0", () => inputNumber("0"), true)}
|
||||
{renderButton(".", inputDecimal)}
|
||||
{renderButton("=", calculate)}
|
||||
</row>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// Main bot setup
|
||||
async function main() {
|
||||
// You'll need to set these environment variables
|
||||
const config = {
|
||||
apiId: parseInt(process.env.API_ID || '0'),
|
||||
apiHash: process.env.API_HASH || '',
|
||||
botToken: process.env.BOT_TOKEN || '',
|
||||
storage: process.env.STORAGE_PATH || '.mtcute'
|
||||
};
|
||||
|
||||
if (!config.apiId || !config.apiHash || !config.botToken) {
|
||||
console.error('Please set API_ID, API_HASH, and BOT_TOKEN environment variables');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const adapter = new MtcuteAdapter(config);
|
||||
|
||||
// Set up command handlers
|
||||
adapter.onCommand('start', () => <Calculator />);
|
||||
adapter.onCommand('calculator', () => <Calculator />);
|
||||
adapter.onCommand('calc', () => <Calculator />);
|
||||
|
||||
// Start the bot
|
||||
await adapter.start(config.botToken);
|
||||
|
||||
console.log('Calculator bot is running! Send /calculator to begin.');
|
||||
}
|
||||
|
||||
// Run the bot
|
||||
main().catch(console.error);
|
||||
426
packages/examples/src/tetris-bot.tsx
Normal file
426
packages/examples/src/tetris-bot.tsx
Normal file
@@ -0,0 +1,426 @@
|
||||
/// <reference types="@react-telegram/core" />
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { MtcuteAdapter } from "@react-telegram/mtcute-adapter";
|
||||
|
||||
// Tetris constants
|
||||
const BOARD_WIDTH = 15;
|
||||
const BOARD_HEIGHT = 15;
|
||||
const TICK_SPEED = 2000; // milliseconds (2 seconds)
|
||||
|
||||
// Piece definitions (7 tetrominos)
|
||||
const PIECES = {
|
||||
I: {
|
||||
shape: [
|
||||
[0, 0, 0, 0],
|
||||
[1, 1, 1, 1],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0]
|
||||
],
|
||||
color: '🟦'
|
||||
},
|
||||
O: {
|
||||
shape: [
|
||||
[1, 1],
|
||||
[1, 1]
|
||||
],
|
||||
color: '🟨'
|
||||
},
|
||||
T: {
|
||||
shape: [
|
||||
[0, 1, 0],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0]
|
||||
],
|
||||
color: '🟪'
|
||||
},
|
||||
S: {
|
||||
shape: [
|
||||
[0, 1, 1],
|
||||
[1, 1, 0],
|
||||
[0, 0, 0]
|
||||
],
|
||||
color: '🟩'
|
||||
},
|
||||
Z: {
|
||||
shape: [
|
||||
[1, 1, 0],
|
||||
[0, 1, 1],
|
||||
[0, 0, 0]
|
||||
],
|
||||
color: '🟥'
|
||||
},
|
||||
J: {
|
||||
shape: [
|
||||
[1, 0, 0],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0]
|
||||
],
|
||||
color: '🟦'
|
||||
},
|
||||
L: {
|
||||
shape: [
|
||||
[0, 0, 1],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0]
|
||||
],
|
||||
color: '🟧'
|
||||
}
|
||||
};
|
||||
|
||||
type PieceType = keyof typeof PIECES;
|
||||
type Board = (string | null)[][];
|
||||
|
||||
interface GameState {
|
||||
board: Board;
|
||||
currentPiece: {
|
||||
type: PieceType;
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
} | null;
|
||||
nextPiece: PieceType;
|
||||
score: number;
|
||||
lines: number;
|
||||
level: number;
|
||||
gameOver: boolean;
|
||||
paused: boolean;
|
||||
}
|
||||
|
||||
const createEmptyBoard = (): Board => {
|
||||
return Array(BOARD_HEIGHT).fill(null).map(() => Array(BOARD_WIDTH).fill(null));
|
||||
};
|
||||
|
||||
const getRandomPiece = (): PieceType => {
|
||||
const pieces = Object.keys(PIECES) as PieceType[];
|
||||
return pieces[Math.floor(Math.random() * pieces.length)];
|
||||
};
|
||||
|
||||
const rotatePiece = (shape: number[][]): number[][] => {
|
||||
const n = shape.length;
|
||||
const rotated = Array(n).fill(null).map(() => Array(n).fill(0));
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let j = 0; j < n; j++) {
|
||||
rotated[j][n - 1 - i] = shape[i][j];
|
||||
}
|
||||
}
|
||||
|
||||
return rotated;
|
||||
};
|
||||
|
||||
const getPieceShape = (type: PieceType, rotation: number): number[][] => {
|
||||
let shape = PIECES[type].shape;
|
||||
for (let i = 0; i < rotation; i++) {
|
||||
shape = rotatePiece(shape);
|
||||
}
|
||||
return shape;
|
||||
};
|
||||
|
||||
const isValidMove = (board: Board, piece: GameState['currentPiece'], dx = 0, dy = 0, newRotation?: number): boolean => {
|
||||
if (!piece) return false;
|
||||
|
||||
const rotation = newRotation !== undefined ? newRotation : piece.rotation;
|
||||
const shape = getPieceShape(piece.type, rotation);
|
||||
const newX = piece.x + dx;
|
||||
const newY = piece.y + dy;
|
||||
|
||||
for (let y = 0; y < shape.length; y++) {
|
||||
for (let x = 0; x < shape[y].length; x++) {
|
||||
if (shape[y][x]) {
|
||||
const boardX = newX + x;
|
||||
const boardY = newY + y;
|
||||
|
||||
if (boardX < 0 || boardX >= BOARD_WIDTH || boardY >= BOARD_HEIGHT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (boardY >= 0 && board[boardY][boardX]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const placePiece = (board: Board, piece: GameState['currentPiece']): Board => {
|
||||
if (!piece) return board;
|
||||
|
||||
const newBoard = board.map(row => [...row]);
|
||||
const shape = getPieceShape(piece.type, piece.rotation);
|
||||
const color = PIECES[piece.type].color;
|
||||
|
||||
for (let y = 0; y < shape.length; y++) {
|
||||
for (let x = 0; x < shape[y].length; x++) {
|
||||
if (shape[y][x] && piece.y + y >= 0) {
|
||||
newBoard[piece.y + y][piece.x + x] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newBoard;
|
||||
};
|
||||
|
||||
const clearLines = (board: Board): { board: Board; linesCleared: number } => {
|
||||
const newBoard = board.filter(row => row.some(cell => !cell));
|
||||
const linesCleared = BOARD_HEIGHT - newBoard.length;
|
||||
|
||||
while (newBoard.length < BOARD_HEIGHT) {
|
||||
newBoard.unshift(Array(BOARD_WIDTH).fill(null));
|
||||
}
|
||||
|
||||
return { board: newBoard, linesCleared };
|
||||
};
|
||||
|
||||
const TetrisGame = () => {
|
||||
const [gameState, setGameState] = useState<GameState>({
|
||||
board: createEmptyBoard(),
|
||||
currentPiece: null,
|
||||
nextPiece: getRandomPiece(),
|
||||
score: 0,
|
||||
lines: 0,
|
||||
level: 1,
|
||||
gameOver: false,
|
||||
paused: false
|
||||
});
|
||||
|
||||
const gameLoopRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Initialize game
|
||||
useEffect(() => {
|
||||
if (!gameState.currentPiece && !gameState.gameOver) {
|
||||
spawnNewPiece();
|
||||
}
|
||||
}, [gameState.currentPiece, gameState.gameOver]);
|
||||
|
||||
// Game loop
|
||||
useEffect(() => {
|
||||
if (gameState.gameOver || gameState.paused) {
|
||||
if (gameLoopRef.current) {
|
||||
clearInterval(gameLoopRef.current);
|
||||
gameLoopRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameState.currentPiece) {
|
||||
gameLoopRef.current = setInterval(() => {
|
||||
movePiece(0, 1);
|
||||
}, TICK_SPEED);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (gameLoopRef.current) {
|
||||
clearInterval(gameLoopRef.current);
|
||||
}
|
||||
};
|
||||
}, [gameState.currentPiece, gameState.gameOver, gameState.paused]);
|
||||
|
||||
const spawnNewPiece = () => {
|
||||
const newPiece = {
|
||||
type: gameState.nextPiece,
|
||||
x: Math.floor(BOARD_WIDTH / 2) - 1,
|
||||
y: 0,
|
||||
rotation: 0
|
||||
};
|
||||
|
||||
if (!isValidMove(gameState.board, newPiece)) {
|
||||
setGameState(prev => ({ ...prev, gameOver: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
setGameState(prev => ({
|
||||
...prev,
|
||||
currentPiece: newPiece,
|
||||
nextPiece: getRandomPiece()
|
||||
}));
|
||||
};
|
||||
|
||||
const movePiece = (dx: number, dy: number) => {
|
||||
setGameState(prev => {
|
||||
if (!prev.currentPiece || prev.gameOver) return prev;
|
||||
|
||||
if (isValidMove(prev.board, prev.currentPiece, dx, dy)) {
|
||||
return {
|
||||
...prev,
|
||||
currentPiece: {
|
||||
...prev.currentPiece,
|
||||
x: prev.currentPiece.x + dx,
|
||||
y: prev.currentPiece.y + dy
|
||||
}
|
||||
};
|
||||
} else if (dy > 0) {
|
||||
// Piece can't move down, place it
|
||||
const boardWithPiece = placePiece(prev.board, prev.currentPiece);
|
||||
const { board: clearedBoard, linesCleared } = clearLines(boardWithPiece);
|
||||
|
||||
const newScore = prev.score + linesCleared * 100 * prev.level + 10;
|
||||
const newLines = prev.lines + linesCleared;
|
||||
const newLevel = Math.floor(newLines / 10) + 1;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
board: clearedBoard,
|
||||
currentPiece: null,
|
||||
score: newScore,
|
||||
lines: newLines,
|
||||
level: newLevel
|
||||
};
|
||||
}
|
||||
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
|
||||
const rotatePieceAction = () => {
|
||||
setGameState(prev => {
|
||||
if (!prev.currentPiece || prev.gameOver) return prev;
|
||||
|
||||
const newRotation = (prev.currentPiece.rotation + 1) % 4;
|
||||
if (isValidMove(prev.board, prev.currentPiece, 0, 0, newRotation)) {
|
||||
return {
|
||||
...prev,
|
||||
currentPiece: {
|
||||
...prev.currentPiece,
|
||||
rotation: newRotation
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
|
||||
const dropPiece = () => {
|
||||
setGameState(prev => {
|
||||
if (!prev.currentPiece || prev.gameOver) return prev;
|
||||
|
||||
let dropDistance = 0;
|
||||
while (isValidMove(prev.board, prev.currentPiece, 0, dropDistance + 1)) {
|
||||
dropDistance++;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
currentPiece: {
|
||||
...prev.currentPiece,
|
||||
y: prev.currentPiece.y + dropDistance
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const togglePause = () => {
|
||||
setGameState(prev => ({ ...prev, paused: !prev.paused }));
|
||||
};
|
||||
|
||||
const resetGame = () => {
|
||||
setGameState({
|
||||
board: createEmptyBoard(),
|
||||
currentPiece: null,
|
||||
nextPiece: getRandomPiece(),
|
||||
score: 0,
|
||||
lines: 0,
|
||||
level: 1,
|
||||
gameOver: false,
|
||||
paused: false
|
||||
});
|
||||
};
|
||||
|
||||
// Render the board with current piece
|
||||
const renderBoard = () => {
|
||||
const displayBoard = gameState.board.map(row => [...row]);
|
||||
|
||||
if (gameState.currentPiece && !gameState.gameOver) {
|
||||
const shape = getPieceShape(gameState.currentPiece.type, gameState.currentPiece.rotation);
|
||||
const color = PIECES[gameState.currentPiece.type].color;
|
||||
|
||||
for (let y = 0; y < shape.length; y++) {
|
||||
for (let x = 0; x < shape[y].length; x++) {
|
||||
if (shape[y][x]) {
|
||||
const boardY = gameState.currentPiece.y + y;
|
||||
const boardX = gameState.currentPiece.x + x;
|
||||
if (boardY >= 0 && boardY < BOARD_HEIGHT && boardX >= 0 && boardX < BOARD_WIDTH) {
|
||||
displayBoard[boardY][boardX] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return displayBoard.map(row =>
|
||||
row.map(cell => cell || '⬛').join('')
|
||||
).join('\n');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<b>🎮 Tetris</b>
|
||||
<br />
|
||||
<br />
|
||||
<code>{renderBoard()}</code>
|
||||
<br />
|
||||
<br />
|
||||
<b>Score:</b> {gameState.score} | <b>Lines:</b> {gameState.lines} | <b>Level:</b> {gameState.level}
|
||||
<br />
|
||||
<b>Next:</b> {PIECES[gameState.nextPiece].color}
|
||||
<br />
|
||||
<br />
|
||||
{gameState.gameOver ? (
|
||||
<>
|
||||
<b>🎮 GAME OVER! 🎮</b>
|
||||
<br />
|
||||
<br />
|
||||
<row>
|
||||
<button onClick={resetGame}>🔄 New Game</button>
|
||||
</row>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<row>
|
||||
<button onClick={() => movePiece(-1, 0)}>⬅️</button>
|
||||
<button onClick={() => movePiece(1, 0)}>➡️</button>
|
||||
<button onClick={rotatePieceAction}>🔄</button>
|
||||
<button onClick={dropPiece}>⬇️</button>
|
||||
</row>
|
||||
<row>
|
||||
<button onClick={togglePause}>
|
||||
{gameState.paused ? '▶️ Resume' : '⏸️ Pause'}
|
||||
</button>
|
||||
<button onClick={resetGame}>🔄 New Game</button>
|
||||
</row>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// Main bot setup
|
||||
async function main() {
|
||||
const config = {
|
||||
apiId: parseInt(process.env.API_ID || '0'),
|
||||
apiHash: process.env.API_HASH || '',
|
||||
botToken: process.env.BOT_TOKEN || '',
|
||||
storage: process.env.STORAGE_PATH || '.mtcute'
|
||||
};
|
||||
|
||||
if (!config.apiId || !config.apiHash || !config.botToken) {
|
||||
console.error('Please set API_ID, API_HASH, and BOT_TOKEN environment variables');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const adapter = new MtcuteAdapter(config);
|
||||
|
||||
// Set up command handlers
|
||||
adapter.onCommand('start', () => <TetrisGame />);
|
||||
adapter.onCommand('tetris', () => <TetrisGame />);
|
||||
|
||||
// Start the bot
|
||||
await adapter.start(config.botToken);
|
||||
|
||||
console.log('Tetris bot is running! Send /tetris to play.');
|
||||
}
|
||||
|
||||
// Run the bot
|
||||
main().catch(console.error);
|
||||
@@ -251,6 +251,8 @@ export class MtcuteAdapter {
|
||||
const textWithEntities = this.rootNodeToTextWithEntities(root);
|
||||
const replyMarkup = this.rootNodeToInlineKeyboard(root, containerId);
|
||||
|
||||
await retryOnRpcError(async () => {
|
||||
|
||||
if (messageId === null) {
|
||||
// First render: send a new message
|
||||
const sentMessage = await this.client.sendText(chatId, textWithEntities, {
|
||||
@@ -264,8 +266,14 @@ export class MtcuteAdapter {
|
||||
message: messageId,
|
||||
text: textWithEntities,
|
||||
replyMarkup
|
||||
}).catch(e => {
|
||||
if (tl.RpcError.is(e) && e.code === 400 && e.text === "MESSAGE_NOT_MODIFIED") {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
// Initial render
|
||||
@@ -292,4 +300,16 @@ export class MtcuteAdapter {
|
||||
getDispatcher() {
|
||||
return this.dispatcher;
|
||||
}
|
||||
}
|
||||
|
||||
async function retryOnRpcError<T>(fn: () => Promise<T>): Promise<T> {
|
||||
return fn().catch(async (error) => {
|
||||
if (tl.RpcError.is(error) && error.is('FLOOD_WAIT_%d')) {
|
||||
const seconds = error.seconds;
|
||||
console.log(`FLOOD_WAIT_${seconds}: Waiting for ${seconds} seconds`);
|
||||
await new Promise(resolve => setTimeout(resolve, seconds * 1000));
|
||||
return await retryOnRpcError(fn);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user