Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions typescript-sdk/apps/dojo/src/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ export const agentsIntegrations: AgentIntegrationConfig[] = [
deploymentUrl: "http://localhost:2024",
graphId: "tool_based_generative_ui",
}),
gomoku: new LangGraphAgent({
deploymentUrl: "http://localhost:2024",
graphId: "gomoku",
}),
};
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"use client";
import { CopilotKit, useCoAgent, useCopilotChat } from "@copilotkit/react-core";
import { CopilotSidebar } from "@copilotkit/react-ui";
import React, { useState } from "react";
import "@copilotkit/react-ui/styles.css";
import "./style.css";
import { Role, TextMessage } from "@copilotkit/runtime-client-gql";

const BOARD_SIZE = 11;
const EMPTY = 0;
const BLACK = 1;
const WHITE = 2;

interface GomokuState {
board: number[][];
current_player: number;
winner?: number | null;
last_move?: { row: number; col: number } | null;
}

const INITIAL_STATE: GomokuState = {
board: Array.from({ length: BOARD_SIZE }, () => Array(BOARD_SIZE).fill(EMPTY)),
current_player: BLACK,
winner: EMPTY,
last_move: null,
};

const ZEN_QUOTES = [
"Observe the changes with a calm mind 🍃",
"A game ends, mind like still water 🪷",
"Victory and defeat are common, peace of mind is key 🧘‍♂️",
"Like fallen cherry blossoms, the game concludes 🌸",
"Wind leaves no trace, but the game's spirit remains 🏯",
"Meeting through Go, endless zen 🪨",
"Watching quietly, mind wanders far 🏞️",
];

const USER_MESSAGES = [
"Stone placed at {row}, {col}. The game flows like water.",
"Moving to {row}, {col}. Like a leaf in the wind.",
"Position {row}, {col} chosen. The path reveals itself.",
"Stone at {row}, {col}. Silence speaks volumes.",
"Playing {row}, {col}. Each move, a new beginning.",
];

const ZEN_EMOJIS = ["🌸", "🍃", "🪷", "🏯", "🧘‍♂️", "🪨", "🏞️", "🎋", "⛩️"];

export default function GomokuPage({ params }: { params: Promise<{ integrationId: string }> }) {
const { integrationId } = React.use(params);
return (
<CopilotKit
runtimeUrl={`/api/copilotkit/${integrationId}`}
showDevConsole={false}
agent="gomoku"
>
<div className="gomoku-page">
<GomokuGame />
<CopilotSidebar
defaultOpen={true}
labels={{
title: "Gomoku AI Assistant",
initial: "Welcome to Gomoku! Let's play a game.",
}}
clickOutsideToClose={false}
/>
</div>
</CopilotKit>
);
}

function GomokuGame() {
const { state: agentState, setState: setAgentState } = useCoAgent<GomokuState>({
name: "gomoku",
initialState: INITIAL_STATE,
});
const { appendMessage, isLoading } = useCopilotChat();
const [showModal, setShowModal] = useState(false);
const [zenMsg, setZenMsg] = useState("");
const [zenEmoji, setZenEmoji] = useState("");
const [footerQuote] = useState(() => ZEN_QUOTES[Math.floor(Math.random() * ZEN_QUOTES.length)]);

React.useEffect(() => {
if (agentState?.winner && !showModal) {
const msg = ZEN_QUOTES[Math.floor(Math.random() * ZEN_QUOTES.length)];
const emoji = ZEN_EMOJIS[Math.floor(Math.random() * ZEN_EMOJIS.length)];
setZenMsg(msg);
setZenEmoji(emoji);
setTimeout(() => setShowModal(true), 600);
}
if (!agentState?.winner) {
setShowModal(false);
}
}, [agentState?.winner]);

if (!agentState) {
return <div>Loading...</div>;
}

const handleCellClick = (row: number, col: number) => {
if (!isLoading && !agentState.winner && agentState.board[row][col] === EMPTY) {
setAgentState(prevState => ({
...prevState!,
last_move: { row, col },
board: prevState!.board.map((rowArr, r) =>
rowArr.map((_, c) => (r === row && c === col ? prevState!.current_player : _)),
),
current_player: prevState!.current_player === BLACK ? WHITE : BLACK,
}));

const randomMessage = USER_MESSAGES[Math.floor(Math.random() * USER_MESSAGES.length)]
.replace("{row}", row.toString())
.replace("{col}", col.toString());

appendMessage(
new TextMessage({
content: randomMessage,
role: Role.User,
}),
);
}
};

const renderCell = (row: number, col: number) => {
const value = agentState.board[row][col];
let cellClass = "gomoku-cell";
if (agentState.last_move && agentState.last_move.row === row && agentState.last_move.col === col) {
cellClass += " gomoku-last-move";
}
return (
<div
key={`${row}-${col}`}
className={cellClass}
onClick={() => handleCellClick(row, col)}
>
{value === BLACK && <span className="gomoku-stone gomoku-black" />}
{value === WHITE && <span className="gomoku-stone gomoku-white" />}
</div>
);
};

return (
<div className="gomoku-zen-container">
<h2 className="section-title">Gomoku <span className="zen-sakura">🌸</span></h2>
<div className="gomoku-status">
{agentState.winner !== EMPTY
? `Winner: ${agentState.winner === BLACK ? "Black (You)" : "White (AI)"}`
: isLoading
? "AI is thinking..."
: `Current Player: ${agentState.current_player === BLACK ? "Black (You)" : "White (AI)"}`}
</div>
{showModal && (
<div className="zen-message">
<span className="zen-message-emoji">{zenEmoji}</span>
<span className="zen-message-text">{zenMsg}</span>
</div>
)}
<div className={`gomoku-board-zen${isLoading ? " gomoku-board-loading" : ""}`}>
{agentState.board.map((rowArr, row) => (
<div key={row} className="gomoku-row-zen">
{rowArr.map((_, col) => renderCell(row, col))}
</div>
))}
</div>
{agentState.winner !== EMPTY && (
<button
className="zen-restart-btn"
onClick={() => setAgentState(INITIAL_STATE)}
>
Play Again 🍵
</button>
)}
<div className="zen-footer">{footerQuote}</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
.gomoku-page {
min-height: 100vh;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f7f3ee;
position: relative;
overflow: auto;
}

.gomoku-zen-container {
background: #f7f3ee;
padding: 40px 32px;
width: 100%;
max-width: 440px;
margin: 20px auto;
position: relative;
z-index: 1;
}

.section-title {
font-family: 'Noto Serif', serif;
font-size: 1.5rem;
font-weight: 400;
color: #5c4b3c;
text-align: center;
margin-bottom: 24px;
letter-spacing: 4px;
}

.zen-sakura {
font-size: 1rem;
vertical-align: middle;
opacity: 0.7;
}

.gomoku-status {
font-family: 'Noto Sans', sans-serif;
color: #7c6f57;
font-size: 0.9rem;
margin-bottom: 24px;
text-align: center;
letter-spacing: 1px;
min-height: 1.2em;
transition: color 0.2s ease;
}

.gomoku-board-zen {
display: grid;
grid-template-rows: repeat(11, minmax(32px, 1fr));
gap: 0;
margin: 24px 0;
background: #f0e6d9;
padding: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
aspect-ratio: 1;
transition: opacity 0.2s ease;
}

.gomoku-board-loading {
opacity: 0.7;
cursor: not-allowed;
}

.gomoku-board-loading .gomoku-cell {
cursor: not-allowed;
pointer-events: none;
}

.gomoku-row-zen {
display: grid;
grid-template-columns: repeat(11, 1fr);
gap: 0;
height: 100%;
}

.gomoku-cell {
aspect-ratio: 1;
background: #f0e6d9;
border: 1px solid #c4b5a2;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
}

.gomoku-cell.gomoku-last-move::after {
content: '';
position: absolute;
top: -1px;
left: -1px;
right: -1px;
bottom: -1px;
border: 2px solid rgba(255, 124, 67, 0.5);
pointer-events: none;
animation: lastMove 1s ease-out forwards;
}

@keyframes lastMove {
0% {
opacity: 1;
transform: scale(1.2);
}
100% {
opacity: 0.6;
transform: scale(1);
}
}

.gomoku-stone {
width: 24px;
height: 24px;
border-radius: 50%;
display: inline-block;
position: relative;
z-index: 1;
}

.gomoku-black {
background: #2c2522;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}

.gomoku-white {
background: #f5f2ed;
border: 1px solid #d9d1c7;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}

.zen-restart-btn {
margin-top: 24px;
padding: 8px 24px;
background: none;
color: #7c6f57;
border: 1px solid #c4b5a2;
font-size: 0.9rem;
font-family: 'Noto Sans', sans-serif;
cursor: pointer;
transition: all 0.2s ease;
letter-spacing: 1px;
display: block;
margin-left: auto;
margin-right: auto;
}

.zen-restart-btn:hover {
background: #f0e6d9;
}

.zen-footer {
color: #a39889;
font-family: 'Noto Serif', serif;
font-size: 0.9rem;
letter-spacing: 2px;
text-align: center;
margin-top: 32px;
user-select: none;
}

.zen-message {
text-align: center;
margin-bottom: 20px;
color: #7c6f57;
font-family: 'Noto Serif', serif;
font-size: 0.9rem;
letter-spacing: 1px;
animation: fadeIn 0.5s ease-out;
}

.zen-message-emoji {
font-size: 1.2rem;
margin-right: 8px;
opacity: 0.8;
}

.zen-message-text {
font-style: italic;
}

@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
6 changes: 6 additions & 0 deletions typescript-sdk/apps/dojo/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ export const featureConfig: FeatureConfig[] = [
description: "Use collaboration to edit a document in real time with your Copilot",
tags: ["State", "Streaming", "Tools"],
}),
createFeatureConfig({
id: "gomoku",
name: "Gomoku",
description: "A game of Gomoku",
tags: ["Game", "Gomoku"],
}),
];

export default featureConfig;
Loading