feat: Coder를 에이전트 모드로 전환 + 리뷰 재시도 루프
핵심 변경: - 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 생성 확인
This commit is contained in:
227
tetris/game.js
Normal file
227
tetris/game.js
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user