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:
2026-03-06 22:13:06 +09:00
parent 83c043863c
commit bccc673713
6 changed files with 483 additions and 187 deletions

227
tetris/game.js Normal file
View 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;
}