Skip to content

Commit

Permalink
add ai mode
Browse files Browse the repository at this point in the history
  • Loading branch information
taroj1205 committed Dec 18, 2023
1 parent c8cb65b commit e217b9d
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 16 deletions.
Binary file modified bun.lockb
Binary file not shown.
40 changes: 40 additions & 0 deletions src/app/api/ai/move/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { GoogleGenerativeAI } from '@google/generative-ai';
import { useSearchParams } from 'next/navigation';

const api = String(process.env.GOOGLE_API_KEY);

const genAI = new GoogleGenerativeAI(api);

export async function POST(request: Request) {
const gameRules = `
This is a Connect 4 game.
1. The game is played on a grid that's 6 cells by 7 cells.
2. Two players take turns.
3. The pieces fall straight down, occupying the lowest available space within the column.
4. The objective of the game is to connect four of one's own discs of the same color next to each other vertically, horizontally, or diagonally before your opponent.
5. The game ends in a tie if the entire board is filled with discs and no player has won.
You are playing as Yellow and your opponent is Red. Suggest the next move in this format: y (just the number of the column).
`


const { grid } = await request.json()

const prompt = grid + gameRules;

if (!prompt) {
return new Response(JSON.stringify({ error: 'No prompt provided' }));
}

try {
const model = genAI.getGenerativeModel({ model: 'gemini-pro' });
const result = await model.generateContent(prompt);
const response = await result.response;
const text = response.text();
console.log(text);
return new Response(JSON.stringify({ move: text }));
} catch (error) {
console.error(error);
return new Response(JSON.stringify({ error }));
}
}
100 changes: 100 additions & 0 deletions src/app/api/compute/next-move/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
function findBestMove(grid: (string | null)[][]): number | null {
// Placeholder for our player's identifier, e.g., "Red" or "Yellow"
const ourPlayer = "Red";
const opponent = "Yellow"; // Assuming a two-player game

// Strategy 1: Check if we need to block the opponent's winning move
let blockingMove = findWinningMove(grid, opponent);
if (blockingMove !== null) {
return blockingMove;
}

// Strategy 2: Check if we can win next
let winningMove = findWinningMove(grid, ourPlayer);
if (winningMove !== null) {
return winningMove;
}

// Strategy 3: Naïve approach - just pick the first non-full column
for (let col = 0; col < grid[0].length; col++) {
if (grid[0][col] === null) {
return col;
}
}

// No moves available
return null;
}

function checkWin(grid: (string | null)[][], player: string): boolean {
// Check horizontal, vertical, and diagonal win conditions
// This is a simple implementation and can be optimized
for (let row = 0; row < grid.length; row++) {
for (let col = 0; col < grid[0].length; col++) {
if (
// Horizontal
(grid[row][col] === player &&
grid[row][col + 1] === player &&
grid[row][col + 2] === player &&
grid[row][col + 3] === player) ||
// Vertical
(grid[row][col] === player &&
grid[row + 1]?.[col] === player &&
grid[row + 2]?.[col] === player &&
grid[row + 3]?.[col] === player) ||
// Diagonal /
(grid[row][col] === player &&
grid[row + 1]?.[col + 1] === player &&
grid[row + 2]?.[col + 2] === player &&
grid[row + 3]?.[col + 3] === player) ||
// Diagonal \
(grid[row][col] === player &&
grid[row + 1]?.[col - 1] === player &&
grid[row + 2]?.[col - 2] === player &&
grid[row + 3]?.[col - 3] === player)
) {
return true;
}
}
}
return false;
}

function findWinningMove(grid: (string | null)[][], player: string): number | null {
// Create a copy of the grid to test potential moves
let testGrid = JSON.parse(JSON.stringify(grid));

// Check each column to see if making a move there would result in a win
for (let col = 0; col < testGrid[0].length; col++) {
for (let row = testGrid.length - 1; row >= 0; row--) {
if (testGrid[row][col] === null) {
testGrid[row][col] = player;
if (checkWin(testGrid, player)) {
return col;
}
// Undo the move
testGrid[row][col] = null;
break;
}
}
}

return null;
}

export async function POST(request: Request) {
const { grid } = await request.json()

if (!grid) {
return new Response(JSON.stringify({ error: 'No grid provided' }));
}

try {
const move = findBestMove(grid);
console.log(move);
return new Response(JSON.stringify({ move }));
} catch (error) {
console.error(error);
return new Response(JSON.stringify({ error }));
}
}
115 changes: 99 additions & 16 deletions src/app/play/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import Confetti from "react-dom-confetti";
import { FaArrowDown } from "react-icons/fa";

Expand All @@ -9,7 +9,7 @@ const Play = () => {
.fill(null)
.map(() => Array(7).fill(null))
);
const [currentPlayer, setCurrentPlayer] = useState("Red");
const currentPlayer = useRef("Red");
const [winner, setWinner] = useState<string | null>(null);
const [lastMove, setLastMove] = useState<[number, number] | null>(null);
const [hoveredCol, setHoveredCol] = useState<boolean[]>(Array(7).fill(false));
Expand All @@ -26,12 +26,14 @@ const Play = () => {
null
);
const [isChecking, setIsChecking] = useState(false);
const [bgColor, setBgColor] = useState("bg-gray-100");

useEffect(() => {
const cellWidth = document.getElementById("board")!.offsetWidth / 7;
const cellHeight = document.getElementById("board")!.offsetHeight / 6;
setCellSize({ width: cellWidth, height: cellHeight });
}, []);
const [playingWithAI, setPlayingWithAI] = useState(false);

const checkWin = (grid: any[]) => {
// Check horizontal, vertical and diagonal directions
Expand Down Expand Up @@ -81,14 +83,14 @@ const Play = () => {
return { winner: null, winningCells: null };
};

const handleClick = (column: number) => {
const handleClick = async (column: number) => {
if (winner || isChecking) return;
setIsChecking(true);
const newGrid = [...grid];
let isColumnFull = true;
for (let i = 5; i >= 0; i--) {
if (!newGrid[i][column]) {
newGrid[i][column] = currentPlayer;
newGrid[i][column] = currentPlayer.current;
setLastMove([i, column]);
setGrid(newGrid);
const cellWidth = document.getElementById("board")!.offsetWidth / 7;
Expand All @@ -109,8 +111,43 @@ const Play = () => {
setIsChecking(false);
}, 500);
} else {
setCurrentPlayer(currentPlayer === "Red" ? "Yellow" : "Red");
setIsChecking(false);
// After the current player makes a move
if (!winner) {
currentPlayer.current =
currentPlayer.current === "Red" ? "Yellow" : "Red";
// If AI mode is on and the current player is yellow, make a request to the AI endpoint
if (playingWithAI && currentPlayer.current === "Yellow") {
const startTime = Date.now();

const response = await fetch("/api/compute/next-move", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ grid: newGrid }),
});

const data = await response.json();

console.log(data);

// Assume the AI endpoint returns the column for the next move
const aiMove = data.move;

// Calculate the elapsed time
const elapsedTime = Date.now() - startTime;

// If less than a second has passed, wait the remaining time
const delay = elapsedTime < 1000 ? 1000 - elapsedTime : 0;

// Make the AI's move
setTimeout(() => {
handleClick(aiMove);
}, delay);
} else {
setIsChecking(false);
}
}
}
isColumnFull = false;
break;
Expand Down Expand Up @@ -141,7 +178,7 @@ const Play = () => {
.fill(null)
.map(() => Array(7).fill(null))
);
setCurrentPlayer("Red");
currentPlayer.current = "Red";
setWinner(null);
setLastMove(null);
setEmpty(
Expand All @@ -152,12 +189,51 @@ const Play = () => {
setWinningCells(null);
};

const playWithAI = () => {
restartGame();
setPlayingWithAI(true);
};

useEffect(() => {
if (playingWithAI && winner === "Yellow") {
let count = 0;
const intervalId = setInterval(() => {
setBgColor(count % 2 === 0 ? "bg-red-500" : "bg-gray-100");
count++;
if (count === 6) {
clearInterval(intervalId);
}
}, 150);
}
}, [playingWithAI, winner]);

return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100 text-black">
<h1 className="text-4xl font-bold mb-4">Play Connect 4!</h1>
<div
className={`flex flex-col items-center justify-center min-h-screen ${bgColor} transition-colors duration-100 text-black`}>
<h1 className="text-4xl font-bold mb-4">
Play Connect 4!{playingWithAI ? " (AI Mode)" : ""}
</h1>
<button
onClick={playWithAI}
disabled={playingWithAI}
className="bg-blue-500 hover:bg-blue-700 disabled:bg-blue-300 text-white font-bold mb-4 py-2 px-4 rounded z-10">
Play with AI
</button>
{winner ? (
<div className="mb-4 z-10 flex items-center justify-center flex-col">
<h2 className="text-2xl mb-4">Winner: {winner}</h2>
<h2 className="text-3xl mb-4">
Winner:{" "}
<span
className={
playingWithAI && winner === "Yellow"
? "text-red-500"
: winner === "Yellow"
? "text-yellow-500"
: "text-red-500"
}>
{playingWithAI && winner === "Yellow" ? "AI" : winner}
</span>
</h2>
<button
onClick={restartGame}
className="mb-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Expand All @@ -169,27 +245,32 @@ const Play = () => {
className="text-2xl px-2 z-10"
style={{
backgroundColor:
currentPlayer === "Red"
currentPlayer.current === "Red"
? "rgba(255, 0, 0, 0.5)"
: "rgba(255, 255, 0, 0.5)",
}}>
Current Player: {currentPlayer}
{playingWithAI && currentPlayer.current === "Yellow" && isChecking
? "Thinking..."
: `Current Player: ${currentPlayer.current}`}
</h2>
)}
<div className="fixed">
<Confetti active={winner !== null} config={config} />
</div>
<div
className="grid-cols-7 z-0 gap-0 w-[35rem] h-auto max-w-[100svw] px-2 sm:px-0"
style={{ display: winner ? "none" : "grid" }}>
style={{
display: winner ? "none" : "grid",
opacity: isChecking ? 0 : 1,
}}>
{Array(7)
.fill(null)
.map((_, index) => (
<div
key={index}
className="relative aspect-square"
style={{
cursor: winner ? "normal" : "pointer",
cursor: winner || isChecking ? "normal" : "pointer",
}}
onMouseEnter={() => {
// if winner or all cells filled in the column
Expand All @@ -200,6 +281,8 @@ const Play = () => {
newHoveredCol[index] = true;
return newHoveredCol;
});

console.log(grid);
}}
onMouseLeave={() => {
setHoveredCol((prev) => {
Expand All @@ -217,7 +300,7 @@ const Play = () => {
className="absolute inset-0 rounded-full transition-opacity duration-300"
style={{
backgroundColor:
currentPlayer === "Red"
currentPlayer.current === "Red"
? "rgba(255, 0, 0, 0.5)"
: "rgba(255, 255, 0, 0.5)",
opacity: hoveredCol[index] ? 1 : 0,
Expand Down Expand Up @@ -259,7 +342,7 @@ const Play = () => {
style={{
borderRightWidth: x === 6 ? 2 : 0,
borderBottomWidth: y === 5 ? 2 : 0,
cursor: winner ? "normal" : "pointer",
cursor: winner || isChecking ? "normal" : "pointer",
backgroundColor: winningCells?.some(
(winningCell) =>
winningCell[0] === x && winningCell[1] === y
Expand Down

0 comments on commit e217b9d

Please sign in to comment.