핵심 변경: - gemini_caller.py: call_agent() 추가 (cwd 지원, 5분 타임아웃) Gemini가 프로젝트 디렉토리에서 직접 파일 읽기/쓰기/실행 - task_pipeline.py: Coder가 call_agent() 사용, file_applier 의존 제거 리뷰 실패 시 최대 2회 재시도 (피드백 포함) - discord_bot.py: pipeline.execute() 호출로 단순화 - coder.md: 파일 직접 쓰기 지시 (코드블록 출력 금지) - 검증: echo prompt | gemini --cwd=VW_Proj → test_agent.txt 생성 확인
228 lines
6.5 KiB
JavaScript
228 lines
6.5 KiB
JavaScript
/**
|
|
* Tetris Game Logic
|
|
* Implements core mechanics: movement, rotation, collision, line clearing, and scoring.
|
|
*/
|
|
|
|
class Tetris {
|
|
constructor(width = 10, height = 20) {
|
|
this.width = width;
|
|
this.height = height;
|
|
this.grid = this.createGrid();
|
|
this.score = 0;
|
|
this.linesCleared = 0;
|
|
this.gameOver = false;
|
|
|
|
// Tetromino shapes definitions
|
|
this.shapes = {
|
|
'I': [[1, 1, 1, 1]],
|
|
'J': [[1, 0, 0], [1, 1, 1]],
|
|
'L': [[0, 0, 1], [1, 1, 1]],
|
|
'O': [[1, 1], [1, 1]],
|
|
'S': [[0, 1, 1], [1, 1, 0]],
|
|
'T': [[0, 1, 0], [1, 1, 1]],
|
|
'Z': [[1, 1, 0], [0, 1, 1]]
|
|
};
|
|
|
|
this.colors = {
|
|
'I': '#00f0f0',
|
|
'J': '#0000f0',
|
|
'L': '#f0a000',
|
|
'O': '#f0f000',
|
|
'S': '#00f000',
|
|
'T': '#a000f0',
|
|
'Z': '#f00000'
|
|
};
|
|
|
|
this.currentPiece = null;
|
|
this.nextPiece = null;
|
|
this.spawnPiece();
|
|
}
|
|
|
|
/**
|
|
* Creates an empty game grid
|
|
*/
|
|
createGrid() {
|
|
return Array.from({ length: this.height }, () => Array(this.width).fill(0));
|
|
}
|
|
|
|
/**
|
|
* Spawns a new random tetromino
|
|
*/
|
|
spawnPiece() {
|
|
const types = Object.keys(this.shapes);
|
|
if (!this.nextPiece) {
|
|
this.nextPiece = types[Math.floor(Math.random() * types.length)];
|
|
}
|
|
|
|
const type = this.nextPiece;
|
|
this.nextPiece = types[Math.floor(Math.random() * types.length)];
|
|
|
|
const shape = this.shapes[type];
|
|
this.currentPiece = {
|
|
type: type,
|
|
shape: shape,
|
|
x: Math.floor((this.width - shape[0].length) / 2),
|
|
y: 0
|
|
};
|
|
|
|
// Check for immediate collision (Game Over)
|
|
if (this.checkCollision(this.currentPiece.x, this.currentPiece.y, this.currentPiece.shape)) {
|
|
this.gameOver = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a piece collides with boundaries or other pieces
|
|
*/
|
|
checkCollision(x, y, shape) {
|
|
for (let row = 0; row < shape.length; row++) {
|
|
for (let col = 0; col < shape[row].length; col++) {
|
|
if (shape[row][col] !== 0) {
|
|
const newX = x + col;
|
|
const newY = y + row;
|
|
|
|
if (newX < 0 || newX >= this.width ||
|
|
newY >= this.height ||
|
|
(newY >= 0 && this.grid[newY][newX] !== 0)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Moves the current piece in a given direction
|
|
*/
|
|
move(dx, dy) {
|
|
if (this.gameOver) return false;
|
|
|
|
if (!this.checkCollision(this.currentPiece.x + dx, this.currentPiece.y + dy, this.currentPiece.shape)) {
|
|
this.currentPiece.x += dx;
|
|
this.currentPiece.y += dy;
|
|
return true;
|
|
}
|
|
|
|
// If moving down and collision occurs, lock the piece
|
|
if (dy > 0) {
|
|
this.lockPiece();
|
|
this.clearLines();
|
|
this.spawnPiece();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Rotates the current piece clockwise
|
|
*/
|
|
rotate() {
|
|
if (this.gameOver) return;
|
|
|
|
const originalShape = this.currentPiece.shape;
|
|
const newShape = originalShape[0].map((_, index) =>
|
|
originalShape.map(row => row[index]).reverse()
|
|
);
|
|
|
|
// Basic "Wall Kick" check
|
|
let offset = 0;
|
|
if (this.checkCollision(this.currentPiece.x, this.currentPiece.y, newShape)) {
|
|
// Try shifting left/right to see if it fits
|
|
if (!this.checkCollision(this.currentPiece.x - 1, this.currentPiece.y, newShape)) {
|
|
offset = -1;
|
|
} else if (!this.checkCollision(this.currentPiece.x + 1, this.currentPiece.y, newShape)) {
|
|
offset = 1;
|
|
} else {
|
|
return; // Can't rotate
|
|
}
|
|
}
|
|
|
|
this.currentPiece.x += offset;
|
|
this.currentPiece.shape = newShape;
|
|
}
|
|
|
|
/**
|
|
* Hard drop the current piece
|
|
*/
|
|
hardDrop() {
|
|
while (this.move(0, 1)) {
|
|
// Keep moving down
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Locks the piece into the grid
|
|
*/
|
|
lockPiece() {
|
|
const { shape, x, y, type } = this.currentPiece;
|
|
shape.forEach((row, rowIndex) => {
|
|
row.forEach((value, colIndex) => {
|
|
if (value !== 0) {
|
|
const gridY = y + rowIndex;
|
|
const gridX = x + colIndex;
|
|
if (gridY >= 0) {
|
|
this.grid[gridY][gridX] = type;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clears full lines and updates score
|
|
*/
|
|
clearLines() {
|
|
let linesCount = 0;
|
|
for (let row = this.height - 1; row >= 0; row--) {
|
|
if (this.grid[row].every(cell => cell !== 0)) {
|
|
this.grid.splice(row, 1);
|
|
this.grid.unshift(Array(this.width).fill(0));
|
|
linesCount++;
|
|
row++; // Check the same row index again after splice
|
|
}
|
|
}
|
|
|
|
if (linesCount > 0) {
|
|
const scoring = [0, 100, 300, 500, 800]; // Standard scoring
|
|
this.score += scoring[linesCount];
|
|
this.linesCleared += linesCount;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the current state for rendering
|
|
*/
|
|
getState() {
|
|
// Return a copy of the grid with the current piece superimposed
|
|
const displayGrid = this.grid.map(row => [...row]);
|
|
|
|
if (this.currentPiece && !this.gameOver) {
|
|
const { shape, x, y, type } = this.currentPiece;
|
|
shape.forEach((row, rowIndex) => {
|
|
row.forEach((value, colIndex) => {
|
|
if (value !== 0) {
|
|
const gridY = y + rowIndex;
|
|
const gridX = x + colIndex;
|
|
if (gridY >= 0 && gridY < this.height && gridX >= 0 && gridX < this.width) {
|
|
displayGrid[gridY][gridX] = type;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
return {
|
|
grid: displayGrid,
|
|
score: this.score,
|
|
lines: this.linesCleared,
|
|
nextPiece: this.nextPiece,
|
|
gameOver: this.gameOver
|
|
};
|
|
}
|
|
}
|
|
|
|
// Export for usage
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = Tetris;
|
|
}
|