diff --git a/.prettierrc.json b/.prettierrc.json
index 650cb88..dc6958f 100644
--- a/.prettierrc.json
+++ b/.prettierrc.json
@@ -1,4 +1,4 @@
{
- "singleQuote": true,
+ "singleQuote": false,
"semi": true
}
diff --git a/common.tsx b/common.tsx
index 2e20f84..c63c1e1 100644
--- a/common.tsx
+++ b/common.tsx
@@ -2,37 +2,51 @@ import Link from "next/link";
import { Game } from "./convex/search";
import { Id } from "./convex/_generated/dataModel";
-function getProfileLink(className: string, name: string, id: string | Id<"users"> | null) {
+function getProfileLink(
+ className: string,
+ name: string,
+ id: string | Id<"users"> | null
+) {
if (!id) {
return <>>;
}
if (typeof id == "string") {
- return {name}
+ return {name};
} else {
- return {name}
+ return (
+
+ {name}
+
+ );
}
}
export function gameTitle(state: Game) {
- const player1Span = getProfileLink("whitePlayer", state.player1Name, state.player1);
- const player2Span = getProfileLink("blackPlayer", state.player2Name, state.player2);
- const context = state.resultContext;
- if (state.player1Name && state.player2Name) {
- return (
+ const player1Span = getProfileLink(
+ "whitePlayer",
+ state.player1Name,
+ state.player1
+ );
+ const player2Span = getProfileLink(
+ "blackPlayer",
+ state.player2Name,
+ state.player2
+ );
+ const context = state.resultContext;
+ if (state.player1Name && state.player2Name) {
+ return (
+
-
{player1Span} vs {player2Span}
- {context && (
-
- {context}
-
- )}
+ {player1Span} vs {player2Span}
- )
- } else if (state.player1Name) {
- return player1Span;
- } else if (state.player2Name) {
- return player2Span;
- } else {
- return
;
- }
+ {context &&
{context}
}
+
+ );
+ } else if (state.player1Name) {
+ return player1Span;
+ } else if (state.player2Name) {
+ return player2Span;
+ } else {
+ return ;
+ }
}
diff --git a/convex/auth.config.js b/convex/auth.config.js
index a62b513..54bbc45 100644
--- a/convex/auth.config.js
+++ b/convex/auth.config.js
@@ -1,8 +1,8 @@
- export default {
- providers: [
+export default {
+ providers: [
{
- "domain": "https://convexdev.us.auth0.com/",
- "applicationID": "tPilWp4HDDF4yoi3D995XMrVIJhflD4V"
- }
+ domain: "https://convexdev.us.auth0.com/",
+ applicationID: "tPilWp4HDDF4yoi3D995XMrVIJhflD4V",
+ },
],
- };
\ No newline at end of file
+};
diff --git a/convex/engine.ts b/convex/engine.ts
index be93ace..f8f4616 100644
--- a/convex/engine.ts
+++ b/convex/engine.ts
@@ -1,45 +1,45 @@
-'use node'
+"use node";
-import { api, internal } from './_generated/api'
-import { Id } from './_generated/dataModel'
-import { internalAction } from './_generated/server'
-const jsChessEngine = require('js-chess-engine')
-import { Chess } from 'chess.js'
+import { api, internal } from "./_generated/api";
+import { Id } from "./_generated/dataModel";
+import { internalAction } from "./_generated/server";
+const jsChessEngine = require("js-chess-engine");
+import { Chess } from "chess.js";
export const maybeMakeComputerMove = internalAction(
- async (ctx, { id }: { id: Id<'games'> }) => {
- const { runQuery, runMutation } = ctx
+ async (ctx, { id }: { id: Id<"games"> }) => {
+ const { runQuery, runMutation } = ctx;
const state = await runQuery(api.games.internalGetPgnForComputerMove, {
id,
- })
+ });
if (state === null) {
- return
+ return;
}
- const [pgn, strategy] = state
- const gameState = new Chess()
- gameState.loadPgn(pgn)
- const moveNumber = gameState.history().length
- const game = new jsChessEngine.Game(gameState.fen())
- let level = 1
- if (strategy === 'hard') {
- level = 2
- } else if (strategy === 'tricky') {
+ const [pgn, strategy] = state;
+ const gameState = new Chess();
+ gameState.loadPgn(pgn);
+ const moveNumber = gameState.history().length;
+ const game = new jsChessEngine.Game(gameState.fen());
+ let level = 1;
+ if (strategy === "hard") {
+ level = 2;
+ } else if (strategy === "tricky") {
if (moveNumber > 6) {
- level = 2
+ level = 2;
if (moveNumber % 3 === 0) {
- level = 3
+ level = 3;
}
}
}
- const aiMove = game.aiMove(level)
+ const aiMove = game.aiMove(level);
// aiMove has format {moveFrom: moveTo}
- let moveFrom = Object.keys(aiMove)[0]
- let moveTo = aiMove[moveFrom]
- console.log(`move at level ${level}: ${moveFrom}->${moveTo}`)
+ let moveFrom = Object.keys(aiMove)[0];
+ let moveTo = aiMove[moveFrom];
+ console.log(`move at level ${level}: ${moveFrom}->${moveTo}`);
await runMutation(internal.games.internalMakeComputerMove, {
id,
moveFrom: moveFrom.toLowerCase(),
moveTo: moveTo.toLowerCase(),
- })
+ });
}
-)
+);
diff --git a/convex/games.ts b/convex/games.ts
index ebd31b2..4456119 100644
--- a/convex/games.ts
+++ b/convex/games.ts
@@ -1,4 +1,4 @@
-import { api, internal } from './_generated/api';
+import { api, internal } from "./_generated/api";
import {
query,
mutation,
@@ -7,32 +7,32 @@ import {
internalMutation,
internalAction,
internalQuery,
-} from './_generated/server';
-import { Id, Doc } from './_generated/dataModel';
+} from "./_generated/server";
+import { Id, Doc } from "./_generated/dataModel";
import {
getCurrentPlayer,
validateMove,
PlayerId,
getNextPlayer,
-} from './utils';
+} from "./utils";
-import { Chess, Move } from 'chess.js';
-import { getOrCreateUser } from './users';
-import { Scheduler } from 'convex/server';
-import { ConvexError, v } from 'convex/values';
-import { chatCompletion } from './lib/openai';
+import { Chess, Move } from "chess.js";
+import { getOrCreateUser } from "./users";
+import { Scheduler } from "convex/server";
+import { ConvexError, v } from "convex/values";
+import { chatCompletion } from "./lib/openai";
async function playerName(
db: DatabaseReader,
- playerId: 'Computer' | Id<'users'> | null
+ playerId: "Computer" | Id<"users"> | null
) {
if (playerId === null) {
- return '';
- } else if (playerId == 'Computer') {
+ return "";
+ } else if (playerId == "Computer") {
return playerId;
} else {
- const user = await db.get(playerId as Id<'users'>);
+ const user = await db.get(playerId as Id<"users">);
if (user === null) {
throw new Error(`Missing player id ${playerId}`);
}
@@ -42,7 +42,7 @@ async function playerName(
export async function denormalizePlayerNames(
db: DatabaseReader,
- game: Doc<'games'>
+ game: Doc<"games">
) {
return {
...game,
@@ -51,7 +51,7 @@ export async function denormalizePlayerNames(
};
}
-export const get = query(async ({ db }, { id }: { id: Id<'games'> }) => {
+export const get = query(async ({ db }, { id }: { id: Id<"games"> }) => {
const game = await db.get(id);
if (!game) {
throw new Error(`Invalid game ${id}`);
@@ -61,9 +61,9 @@ export const get = query(async ({ db }, { id }: { id: Id<'games'> }) => {
export const ongoingGames = query(async ({ db }) => {
const games = await db
- .query('games')
- .withIndex('finished', (q) => q.eq('finished', false))
- .order('desc')
+ .query("games")
+ .withIndex("finished", (q) => q.eq("finished", false))
+ .order("desc")
.take(50);
const result = [];
for (let game of games) {
@@ -79,13 +79,13 @@ export const newGame = mutation(
player1,
player2,
}: {
- player1: null | 'Computer' | 'Me';
- player2: null | 'Computer' | 'Me';
+ player1: null | "Computer" | "Me";
+ player2: null | "Computer" | "Me";
}
) => {
const userId = await getOrCreateUser(db, auth);
let player1Id: PlayerId;
- if (player1 === 'Me') {
+ if (player1 === "Me") {
if (!userId) {
throw new Error("Can't play as unauthenticated user");
}
@@ -94,7 +94,7 @@ export const newGame = mutation(
player1Id = player1;
}
let player2Id: PlayerId;
- if (player2 === 'Me') {
+ if (player2 === "Me") {
if (!userId) {
throw new Error("Can't play as unauthenticated user");
}
@@ -104,7 +104,7 @@ export const newGame = mutation(
}
const game = new Chess();
- let id: Id<'games'> = await db.insert('games', {
+ let id: Id<"games"> = await db.insert("games", {
pgn: game.pgn(),
player1: player1Id,
player2: player2Id,
@@ -118,10 +118,10 @@ export const newGame = mutation(
);
export const joinGame = mutation(
- async ({ db, auth }, { id }: { id: Id<'games'> }) => {
+ async ({ db, auth }, { id }: { id: Id<"games"> }) => {
const user = await getOrCreateUser(db, auth);
if (!user) {
- throw new Error('Trying to join game with unauthenticated user');
+ throw new Error("Trying to join game with unauthenticated user");
}
let state = await db.get(id);
if (state == null) {
@@ -144,7 +144,7 @@ async function _performMove(
db: DatabaseWriter,
player: PlayerId,
scheduler: Scheduler,
- state: Doc<'games'>,
+ state: Doc<"games">,
from: string,
to: string
) {
@@ -190,13 +190,13 @@ async function _performMove(
const boardView = (chess: Chess): string => {
const rows = [];
for (const row of chess.board()) {
- let rowView = '';
+ let rowView = "";
for (const square of row) {
if (square === null) {
- rowView += '.';
+ rowView += ".";
} else {
let piece = square.type as string;
- if (square.color === 'w') {
+ if (square.color === "w") {
piece = piece.toUpperCase();
}
rowView += piece;
@@ -204,12 +204,12 @@ const boardView = (chess: Chess): string => {
}
rows.push(rowView);
}
- return rows.join('\n');
+ return rows.join("\n");
};
export const analyzeMove = internalAction({
args: {
- gameId: v.id('games'),
+ gameId: v.id("games"),
moveIndex: v.number(),
previousPGN: v.string(),
move: v.string(),
@@ -224,12 +224,12 @@ export const analyzeMove = internalAction({
const response = await chatCompletion({
messages: [
{
- role: 'user',
+ role: "user",
content: prompt,
},
],
});
- let responseText = '';
+ let responseText = "";
for await (const chunk of response.content.read()) {
responseText += chunk;
@@ -245,15 +245,15 @@ export const analyzeMove = internalAction({
export const saveAnalysis = internalMutation({
args: {
- gameId: v.id('games'),
+ gameId: v.id("games"),
moveIndex: v.number(),
analysis: v.string(),
},
handler: async (ctx, { gameId, moveIndex, analysis }) => {
const analysisDoc = await ctx.db
- .query('analysis')
- .withIndex('by_game_index', (q) =>
- q.eq('game', gameId).eq('moveIndex', moveIndex)
+ .query("analysis")
+ .withIndex("by_game_index", (q) =>
+ q.eq("game", gameId).eq("moveIndex", moveIndex)
)
.unique();
if (analysisDoc) {
@@ -261,7 +261,7 @@ export const saveAnalysis = internalMutation({
analysis,
});
} else {
- await ctx.db.insert('analysis', {
+ await ctx.db.insert("analysis", {
game: gameId,
moveIndex,
analysis,
@@ -271,26 +271,26 @@ export const saveAnalysis = internalMutation({
});
export const getAnalysis = query({
- args: { gameId: v.id('games'), moveIndex: v.optional(v.number()) },
+ args: { gameId: v.id("games"), moveIndex: v.optional(v.number()) },
handler: async (ctx, { gameId, moveIndex }) => {
const state = await ctx.db.get(gameId);
if (state === null) {
- throw new Error('Invalid Game ID');
+ throw new Error("Invalid Game ID");
}
let analysis;
if (moveIndex !== undefined) {
analysis = await ctx.db
- .query('analysis')
- .withIndex('by_game_index', (q) =>
- q.eq('game', gameId).eq('moveIndex', moveIndex)
+ .query("analysis")
+ .withIndex("by_game_index", (q) =>
+ q.eq("game", gameId).eq("moveIndex", moveIndex)
)
.unique();
} else {
analysis = await ctx.db
- .query('analysis')
- .withIndex('by_game_index', (q) => q.eq('game', gameId))
- .order('desc')
+ .query("analysis")
+ .withIndex("by_game_index", (q) => q.eq("game", gameId))
+ .order("desc")
.first();
}
@@ -314,11 +314,11 @@ export const getAnalysis = query({
export const move = mutation(
async (
{ db, auth, scheduler },
- { gameId, from, to }: { gameId: Id<'games'>; from: string; to: string }
+ { gameId, from, to }: { gameId: Id<"games">; from: string; to: string }
) => {
const userId = await getOrCreateUser(db, auth);
if (!userId) {
- throw new Error('Trying to perform a move with unauthenticated user');
+ throw new Error("Trying to perform a move with unauthenticated user");
}
// Load the game.
@@ -332,13 +332,13 @@ export const move = mutation(
);
export const internalGetPgnForComputerMove = query(
- async ({ db }, { id }: { id: Id<'games'> }) => {
+ async ({ db }, { id }: { id: Id<"games"> }) => {
let state = await db.get(id);
if (state == null) {
throw new Error(`Invalid game ${id}`);
}
- if (getCurrentPlayer(state) !== 'Computer') {
+ if (getCurrentPlayer(state) !== "Computer") {
console.log("it's not the computer's turn");
return null;
}
@@ -348,20 +348,20 @@ export const internalGetPgnForComputerMove = query(
const possibleMoves = game.moves({ verbose: true });
if (game.isGameOver() || game.isDraw() || possibleMoves.length === 0) {
- console.log('no moves');
+ console.log("no moves");
return null;
}
const opponent = getNextPlayer(state);
- let strategy = 'default';
- if (opponent !== 'Computer') {
- const opponentPlayer = await db.get(opponent as Id<'users'>);
+ let strategy = "default";
+ if (opponent !== "Computer") {
+ const opponentPlayer = await db.get(opponent as Id<"users">);
const name = opponentPlayer!.name.toLowerCase();
- if (name.includes('nipunn')) {
- strategy = 'tricky';
- } else if (name.includes('preslav')) {
- strategy = 'hard';
+ if (name.includes("nipunn")) {
+ strategy = "tricky";
+ } else if (name.includes("preslav")) {
+ strategy = "hard";
}
}
@@ -377,7 +377,7 @@ export const internalMakeComputerMove = internalMutation(
moveFrom,
moveTo,
}: {
- id: Id<'games'>;
+ id: Id<"games">;
moveFrom: string;
moveTo: string;
}
@@ -386,9 +386,9 @@ export const internalMakeComputerMove = internalMutation(
if (state == null) {
throw new Error(`Invalid game ${id}`);
}
- if (getCurrentPlayer(state) !== 'Computer') {
+ if (getCurrentPlayer(state) !== "Computer") {
return;
}
- await _performMove(db, 'Computer', scheduler, state, moveFrom, moveTo);
+ await _performMove(db, "Computer", scheduler, state, moveFrom, moveTo);
}
);
diff --git a/convex/lib/openai.ts b/convex/lib/openai.ts
index d13840f..6cdf7ef 100644
--- a/convex/lib/openai.ts
+++ b/convex/lib/openai.ts
@@ -1,25 +1,29 @@
// That's right! No imports and no dependencies 🤯
export async function chatCompletion(
- body: Omit & {
- model?: CreateChatCompletionRequest['model'];
- },
+ body: Omit & {
+ model?: CreateChatCompletionRequest["model"];
+ }
) {
checkForAPIKey();
- body.model = body.model ?? 'gpt-3.5-turbo-16k';
+ body.model = body.model ?? "gpt-3.5-turbo-16k";
body.stream = true;
- const stopWords = body.stop ? (typeof body.stop === 'string' ? [body.stop] : body.stop) : [];
+ const stopWords = body.stop
+ ? typeof body.stop === "string"
+ ? [body.stop]
+ : body.stop
+ : [];
const {
result: resultStream,
retries,
ms,
} = await retryWithBackoff(async () => {
- const result = await fetch('https://api.openai.com/v1/chat/completions', {
- method: 'POST',
+ const result = await fetch("https://api.openai.com/v1/chat/completions", {
+ method: "POST",
headers: {
- 'Content-Type': 'application/json',
- Authorization: 'Bearer ' + process.env.OPENAI_API_KEY,
+ "Content-Type": "application/json",
+ Authorization: "Bearer " + process.env.OPENAI_API_KEY,
},
body: JSON.stringify(body),
@@ -28,7 +32,9 @@ export async function chatCompletion(
throw {
retry: result.status === 429 || result.status >= 500,
error: new Error(
- `Chat completion failed with code ${result.status}: ${await result.text()}`,
+ `Chat completion failed with code ${
+ result.status
+ }: ${await result.text()}`
),
};
}
@@ -48,29 +54,31 @@ export async function fetchEmbeddingBatch(texts: string[]) {
retries,
ms,
} = await retryWithBackoff(async () => {
- const result = await fetch('https://api.openai.com/v1/embeddings', {
- method: 'POST',
+ const result = await fetch("https://api.openai.com/v1/embeddings", {
+ method: "POST",
headers: {
- 'Content-Type': 'application/json',
- Authorization: 'Bearer ' + process.env.OPENAI_API_KEY,
+ "Content-Type": "application/json",
+ Authorization: "Bearer " + process.env.OPENAI_API_KEY,
},
body: JSON.stringify({
- model: 'text-embedding-ada-002',
- input: texts.map((text) => text.replace(/\n/g, ' ')),
+ model: "text-embedding-ada-002",
+ input: texts.map((text) => text.replace(/\n/g, " ")),
}),
});
if (!result.ok) {
throw {
retry: result.status === 429 || result.status >= 500,
- error: new Error(`Embedding failed with code ${result.status}: ${await result.text()}`),
+ error: new Error(
+ `Embedding failed with code ${result.status}: ${await result.text()}`
+ ),
};
}
return (await result.json()) as CreateEmbeddingResponse;
});
if (json.data.length !== texts.length) {
console.error(json);
- throw new Error('Unexpected number of embeddings');
+ throw new Error("Unexpected number of embeddings");
}
const allembeddings = json.data;
allembeddings.sort((a, b) => b.index - a.index);
@@ -90,11 +98,11 @@ export async function fetchEmbedding(text: string) {
export async function fetchModeration(content: string) {
checkForAPIKey();
const { result: flagged } = await retryWithBackoff(async () => {
- const result = await fetch('https://api.openai.com/v1/moderations', {
- method: 'POST',
+ const result = await fetch("https://api.openai.com/v1/moderations", {
+ method: "POST",
headers: {
- 'Content-Type': 'application/json',
- Authorization: 'Bearer ' + process.env.OPENAI_API_KEY,
+ "Content-Type": "application/json",
+ Authorization: "Bearer " + process.env.OPENAI_API_KEY,
},
body: JSON.stringify({
@@ -104,7 +112,9 @@ export async function fetchModeration(content: string) {
if (!result.ok) {
throw {
retry: result.status === 429 || result.status >= 500,
- error: new Error(`Embedding failed with code ${result.status}: ${await result.text()}`),
+ error: new Error(
+ `Embedding failed with code ${result.status}: ${await result.text()}`
+ ),
};
}
return (await result.json()) as { results: { flagged: boolean }[] };
@@ -115,9 +125,9 @@ export async function fetchModeration(content: string) {
const checkForAPIKey = () => {
if (!process.env.OPENAI_API_KEY) {
throw new Error(
- 'Missing OPENAI_API_KEY in environment variables.\n' +
- 'Set it in the project settings in the Convex dashboard:\n' +
- ' npx convex dashboard\n or https://dashboard.convex.dev',
+ "Missing OPENAI_API_KEY in environment variables.\n" +
+ "Set it in the project settings in the Convex dashboard:\n" +
+ " npx convex dashboard\n or https://dashboard.convex.dev"
);
}
};
@@ -128,7 +138,7 @@ const RETRY_JITTER = 100; // In ms
type RetryError = { retry: boolean; error: any };
export async function retryWithBackoff(
- fn: () => Promise,
+ fn: () => Promise
): Promise<{ retries: number; result: T; ms: number }> {
let i = 0;
for (; i <= RETRY_BACKOFF.length; i++) {
@@ -142,11 +152,13 @@ export async function retryWithBackoff(
if (i < RETRY_BACKOFF.length) {
if (retryError.retry) {
console.log(
- `Attempt ${i + 1} failed, waiting ${RETRY_BACKOFF[i]}ms to retry...`,
- Date.now(),
+ `Attempt ${i + 1} failed, waiting ${
+ RETRY_BACKOFF[i]
+ }ms to retry...`,
+ Date.now()
);
await new Promise((resolve) =>
- setTimeout(resolve, RETRY_BACKOFF[i] + RETRY_JITTER * Math.random()),
+ setTimeout(resolve, RETRY_BACKOFF[i] + RETRY_JITTER * Math.random())
);
continue;
}
@@ -155,7 +167,7 @@ export async function retryWithBackoff(
else throw e;
}
}
- throw new Error('Unreachable');
+ throw new Error("Unreachable");
}
// Lifted from openai's package
@@ -170,7 +182,7 @@ export interface LLMMessage {
* The role of the messages author. One of `system`, `user`, `assistant`, or
* `function`.
*/
- role: 'system' | 'user' | 'assistant' | 'function';
+ role: "system" | "user" | "assistant" | "function";
/**
* The name of the author of this message. `name` is required if role is
@@ -217,14 +229,14 @@ export interface CreateChatCompletionRequest {
* @memberof CreateChatCompletionRequest
*/
model:
- | 'gpt-4'
- | 'gpt-4-0613'
- | 'gpt-4-32k'
- | 'gpt-4-32k-0613'
- | 'gpt-3.5-turbo'
- | 'gpt-3.5-turbo-0613'
- | 'gpt-3.5-turbo-16k' // <- our default
- | 'gpt-3.5-turbo-16k-0613';
+ | "gpt-4"
+ | "gpt-4-0613"
+ | "gpt-4-32k"
+ | "gpt-4-32k-0613"
+ | "gpt-3.5-turbo"
+ | "gpt-3.5-turbo-0613"
+ | "gpt-3.5-turbo-16k" // <- our default
+ | "gpt-3.5-turbo-16k-0613";
/**
* The messages to generate chat completions for, in the chat format:
* https://platform.openai.com/docs/guides/chat/introduction
@@ -340,7 +352,7 @@ export interface CreateChatCompletionRequest {
* - "none" is the default when no functions are present.
* - "auto" is the default if functions are present.
*/
- function_call?: 'none' | 'auto' | { name: string };
+ function_call?: "none" | "auto" | { name: string };
}
// Checks whether a suffix of s1 is a prefix of s2. For example,
@@ -368,9 +380,9 @@ export class ChatCompletionContent {
async *readInner() {
for await (const data of this.splitStream(this.body)) {
- if (data.startsWith('data: ')) {
+ if (data.startsWith("data: ")) {
try {
- const json = JSON.parse(data.substring('data: '.length)) as {
+ const json = JSON.parse(data.substring("data: ".length)) as {
choices: { delta: { content?: string } }[];
};
if (json.choices[0].delta.content) {
@@ -386,7 +398,7 @@ export class ChatCompletionContent {
// stop words in OpenAI api don't always work.
// So we have to truncate on our side.
async *read() {
- let lastFragment = '';
+ let lastFragment = "";
for await (const data of this.readInner()) {
lastFragment += data;
let hasOverlap = false;
@@ -402,13 +414,13 @@ export class ChatCompletionContent {
}
if (hasOverlap) continue;
yield lastFragment;
- lastFragment = '';
+ lastFragment = "";
}
yield lastFragment;
}
async readAll() {
- let allContent = '';
+ let allContent = "";
for await (const chunk of this.read()) {
allContent += chunk;
}
@@ -417,26 +429,26 @@ export class ChatCompletionContent {
async *splitStream(stream: ReadableStream) {
const reader = stream.getReader();
- let lastFragment = '';
+ let lastFragment = "";
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
// Flush the last fragment now that we're done
if (lastFragment !== "") {
- yield lastFragment
+ yield lastFragment;
}
- break
+ break;
}
const data = new TextDecoder().decode(value);
lastFragment += data;
- const parts = lastFragment.split("\n\n")
+ const parts = lastFragment.split("\n\n");
// Yield all except for the last part
for (let i = 0; i < parts.length - 1; i += 1) {
- yield parts[i]
+ yield parts[i];
}
// Save the last part as the new last fragment
- lastFragment = parts[parts.length - 1]
+ lastFragment = parts[parts.length - 1];
}
} finally {
reader.releaseLock();
diff --git a/convex/schema.ts b/convex/schema.ts
index 323a3c4..d308915 100644
--- a/convex/schema.ts
+++ b/convex/schema.ts
@@ -1,27 +1,27 @@
-import { defineSchema, defineTable } from 'convex/server'
-import { v } from 'convex/values'
+import { defineSchema, defineTable } from "convex/server";
+import { v } from "convex/values";
export default defineSchema({
games: defineTable({
pgn: v.string(),
- player1: v.union(v.id('users'), v.literal('Computer'), v.null()),
- player2: v.union(v.id('users'), v.literal('Computer'), v.null()),
+ player1: v.union(v.id("users"), v.literal("Computer"), v.null()),
+ player2: v.union(v.id("users"), v.literal("Computer"), v.null()),
finished: v.boolean(),
})
- .index('finished', ['finished'])
- .searchIndex('search_pgn', { searchField: 'pgn' }),
+ .index("finished", ["finished"])
+ .searchIndex("search_pgn", { searchField: "pgn" }),
analysis: defineTable({
game: v.id("games"),
moveIndex: v.number(),
analysis: v.string(),
})
- .index('by_game_index', ['game', 'moveIndex'])
- .searchIndex('search_analysis', { searchField: 'analysis' }),
+ .index("by_game_index", ["game", "moveIndex"])
+ .searchIndex("search_analysis", { searchField: "analysis" }),
users: defineTable({
name: v.string(),
tokenIdentifier: v.string(),
profilePic: v.optional(v.union(v.string(), v.null())),
})
- .index('by_token', ['tokenIdentifier'])
- .searchIndex('search_name', { searchField: 'name' }),
-})
+ .index("by_token", ["tokenIdentifier"])
+ .searchIndex("search_name", { searchField: "name" }),
+});
diff --git a/convex/search.ts b/convex/search.ts
index ff2e5b9..b7c1672 100644
--- a/convex/search.ts
+++ b/convex/search.ts
@@ -1,8 +1,8 @@
-import { denormalizePlayerNames } from './games';
-import { Doc } from './_generated/dataModel';
-import { query } from './_generated/server';
+import { denormalizePlayerNames } from "./games";
+import { Doc } from "./_generated/dataModel";
+import { query } from "./_generated/server";
-export interface Game extends Doc<'games'> {
+export interface Game extends Doc<"games"> {
player1Name: string;
player2Name: string;
moveIndex?: number;
@@ -10,22 +10,22 @@ export interface Game extends Doc<'games'> {
}
type SearchResult = {
- users: Doc<'users'>[];
+ users: Doc<"users">[];
games: Game[];
};
export default query(async ({ db }, { query }: { query: string }) => {
const users = await db
- .query('users')
- .withSearchIndex('search_name', (q) => q.search('name', query))
+ .query("users")
+ .withSearchIndex("search_name", (q) => q.search("name", query))
.collect();
const games = await db
- .query('games')
- .withSearchIndex('search_pgn', (q) => q.search('pgn', query))
+ .query("games")
+ .withSearchIndex("search_pgn", (q) => q.search("pgn", query))
.take(5);
const analyses = await db
- .query('analysis')
- .withSearchIndex('search_analysis', (q) => q.search('analysis', query))
+ .query("analysis")
+ .withSearchIndex("search_analysis", (q) => q.search("analysis", query))
.take(5);
let denormalizedGames = [];
for (const game of games) {
diff --git a/convex/users.ts b/convex/users.ts
index be2ef81..d216e6b 100644
--- a/convex/users.ts
+++ b/convex/users.ts
@@ -1,96 +1,92 @@
-import { v } from 'convex/values'
-import {
- DatabaseWriter,
- mutation,
- query,
-} from './_generated/server'
+import { v } from "convex/values";
+import { DatabaseWriter, mutation, query } from "./_generated/server";
export const getOrCreateUser = async (db: DatabaseWriter, auth: any) => {
- const identity = await auth.getUserIdentity()
+ const identity = await auth.getUserIdentity();
if (!identity) {
- return null
+ return null;
}
// Check if we've already stored this identity before.
const user = await db
- .query('users')
- .withIndex('by_token', (q) =>
- q.eq('tokenIdentifier', identity.tokenIdentifier)
+ .query("users")
+ .withIndex("by_token", (q) =>
+ q.eq("tokenIdentifier", identity.tokenIdentifier)
)
- .unique()
+ .unique();
if (user !== null) {
// If we've seen this identity before but the name has changed, patch the value.
if (user.name != identity.name) {
- await db.patch(user._id, { name: identity.name })
+ await db.patch(user._id, { name: identity.name });
}
- return user._id
+ return user._id;
}
// If it's a new identity, create a new `User`.
- return db.insert('users', {
+ return db.insert("users", {
name: identity.name,
tokenIdentifier: identity.tokenIdentifier,
profilePic: null,
- })
-}
+ });
+};
export const getMyUser = query(async ({ db, auth }) => {
- const identity = await auth.getUserIdentity()
+ const identity = await auth.getUserIdentity();
if (!identity) {
- return null
+ return null;
}
const user = await db
- .query('users')
- .withIndex('by_token', (q) =>
- q.eq('tokenIdentifier', identity.tokenIdentifier)
+ .query("users")
+ .withIndex("by_token", (q) =>
+ q.eq("tokenIdentifier", identity.tokenIdentifier)
)
- .unique()
+ .unique();
- return user?._id
-})
+ return user?._id;
+});
export const get = query({
args: {
- id: v.id('users'),
+ id: v.id("users"),
},
handler: async ({ db, storage }, { id }) => {
- const user = await db.get(id)
- let profilePicUrl = null
+ const user = await db.get(id);
+ let profilePicUrl = null;
if (user?.profilePic) {
- profilePicUrl = await storage.getUrl(user.profilePic)
+ profilePicUrl = await storage.getUrl(user.profilePic);
}
return {
...user,
profilePicUrl,
- }
+ };
},
-})
+});
// Generate a short-lived upload URL.
export const generateUploadUrl = mutation(async ({ storage }) => {
- return await storage.generateUploadUrl()
-})
+ return await storage.generateUploadUrl();
+});
// Save the storage ID within a message.
export const setProfilePic = mutation(
async ({ db, auth }, { storageId }: { storageId: string }) => {
- const identity = await auth.getUserIdentity()
+ const identity = await auth.getUserIdentity();
if (!identity) {
- return null
+ return null;
}
const user = await db
- .query('users')
- .withIndex('by_token', (q) =>
- q.eq('tokenIdentifier', identity.tokenIdentifier)
+ .query("users")
+ .withIndex("by_token", (q) =>
+ q.eq("tokenIdentifier", identity.tokenIdentifier)
)
- .unique()
+ .unique();
if (user === null) {
- throw new Error('Updating profile pic for missing user')
+ throw new Error("Updating profile pic for missing user");
}
- db.patch(user._id, { profilePic: storageId })
+ db.patch(user._id, { profilePic: storageId });
}
-)
+);
diff --git a/convex/utils.ts b/convex/utils.ts
index a6097d4..c48b380 100644
--- a/convex/utils.ts
+++ b/convex/utils.ts
@@ -1,87 +1,87 @@
-import { Chess } from 'chess.js'
-import { Doc, Id } from '../convex/_generated/dataModel'
-import { DatabaseReader } from './_generated/server'
+import { Chess } from "chess.js";
+import { Doc, Id } from "../convex/_generated/dataModel";
+import { DatabaseReader } from "./_generated/server";
-export type PlayerId = Id<'users'> | 'Computer' | null
+export type PlayerId = Id<"users"> | "Computer" | null;
export async function playerName(
db: DatabaseReader,
player: PlayerId
): Promise {
- if (player === 'Computer') {
- return 'Computer'
+ if (player === "Computer") {
+ return "Computer";
}
if (player === null) {
- return 'nobody'
+ return "nobody";
}
- const p = await db.get(player)
+ const p = await db.get(player);
if (!p) {
- return 'invalid-player-id'
+ return "invalid-player-id";
}
- return p.name
+ return p.name;
}
export function playerEquals(player1: PlayerId, player2: PlayerId) {
if (!player1) {
// null is not equal to null.
- return false
+ return false;
}
- return typeof player1 == 'string' ? player1 == player2 : player1 === player2
+ return typeof player1 == "string" ? player1 == player2 : player1 === player2;
}
-export function isOpen(state: Doc<'games'>): boolean {
- return !state.player1 || !state.player2
+export function isOpen(state: Doc<"games">): boolean {
+ return !state.player1 || !state.player2;
}
-export function hasPlayer(state: Doc<'games'>, player: PlayerId): boolean {
+export function hasPlayer(state: Doc<"games">, player: PlayerId): boolean {
if (!player) {
- return false
+ return false;
}
return (
playerEquals(state.player1 as any, player) ||
playerEquals(state.player2 as any, player)
- )
+ );
}
-export function getCurrentPlayer(state: Doc<'games'>): PlayerId {
- const game = new Chess()
- game.loadPgn(state.pgn)
- let result = game.turn() == 'w' ? state.player1 : state.player2
- return result as any
+export function getCurrentPlayer(state: Doc<"games">): PlayerId {
+ const game = new Chess();
+ game.loadPgn(state.pgn);
+ let result = game.turn() == "w" ? state.player1 : state.player2;
+ return result as any;
}
-export function getNextPlayer(state: Doc<'games'>): PlayerId {
- const game = new Chess()
- game.loadPgn(state.pgn)
- let result = game.turn() == 'w' ? state.player2 : state.player1
- return result as any
+export function getNextPlayer(state: Doc<"games">): PlayerId {
+ const game = new Chess();
+ game.loadPgn(state.pgn);
+ let result = game.turn() == "w" ? state.player2 : state.player1;
+ return result as any;
}
export function validateMove(
- state: Doc<'games'>,
+ state: Doc<"games">,
player: PlayerId,
from: string,
to: string
): Chess | null {
if (!playerEquals(getCurrentPlayer(state), player)) {
// Wrong player.
- return null
+ return null;
}
- const game = new Chess()
- game.loadPgn(state.pgn)
- let valid = null
+ const game = new Chess();
+ game.loadPgn(state.pgn);
+ let valid = null;
try {
- valid = game.move({ from, to })
+ valid = game.move({ from, to });
} catch {
// This is lame but try promoting.
try {
- valid = game.move({ from, to, promotion: 'q' })
- console.log('promoted a pawn')
+ valid = game.move({ from, to, promotion: "q" });
+ console.log("promoted a pawn");
} catch {
- console.log(`invalid move ${from}->${to}`)
- valid = null
+ console.log(`invalid move ${from}->${to}`);
+ valid = null;
}
}
- return valid ? game : null
+ return valid ? game : null;
}
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 1fefee8..8eb4206 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -1,15 +1,15 @@
import { api } from "../convex/_generated/api";
-import '../styles/globals.css'
-import type { AppProps } from 'next/app'
+import "../styles/globals.css";
+import type { AppProps } from "next/app";
-import { ConvexReactClient, useQuery } from 'convex/react'
-import { ConvexProviderWithAuth0 } from 'convex/react-auth0'
+import { ConvexReactClient, useQuery } from "convex/react";
+import { ConvexProviderWithAuth0 } from "convex/react-auth0";
-import { useAuth0, Auth0Provider } from '@auth0/auth0-react';
-import { useState } from 'react';
-import Link from 'next/link';
-import { gameTitle } from '../common';
-import { Game } from '../convex/search';
+import { useAuth0, Auth0Provider } from "@auth0/auth0-react";
+import { useState } from "react";
+import Link from "next/link";
+import { gameTitle } from "../common";
+import { Game } from "../convex/search";
const address = process.env.NEXT_PUBLIC_CONVEX_URL;
if (!address) {
@@ -24,19 +24,18 @@ function App(props: AppProps) {
domain={process.env.NEXT_PUBLIC_AUTH0_DOMAIN!}
clientId={process.env.NEXT_PUBLIC_CLIENT_ID!}
authorizationParams={{
- redirect_uri: typeof window !== "undefined" ? window.location.origin : "",
+ redirect_uri:
+ typeof window !== "undefined" ? window.location.origin : "",
}}
useRefreshTokens={true}
cacheLocation="localstorage"
>
-
+
- )
+ );
}
function MyApp({ Component, pageProps }: AppProps) {
@@ -51,53 +50,75 @@ function MyApp({ Component, pageProps }: AppProps) {
setSearchInput(e.target.value);
};
- const searchResults = useQuery(api.search.default, { query: searchInput }) || {users: [], games: []};
+ const searchResults = useQuery(api.search.default, {
+ query: searchInput,
+ }) || { users: [], games: [] };
return (
-
![](/convex.svg)
+
+
+
-
-
-
- {searchResults.users.map((result) =>
-
-
- {
- {(result as any).name}
- }
- |
-
- )}
- {searchResults.games.map((result) =>
-
-
- {gameTitle(result)}
- |
-
- )}
-
+ value={searchInput}
+ />
+
+
+
+ {searchResults.users.map((result) => (
+
+
+ {
+
+ {(result as any).name}
+
+ }
+ |
+
+ ))}
+ {searchResults.games.map((result) => (
+
+
+
+ {gameTitle(result)}
+
+ |
+
+ ))}
+
Convex Chess
- {
- user ?
-
- { user.name }
-
-
- :
- }
+ {user ? (
+
+
+ {user.name}
+
+
+
+ ) : (
+
+ )}
- )
+ );
}
-export default App
+export default App;
diff --git a/pages/index.tsx b/pages/index.tsx
index 3b68c0d..68128a5 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -1,14 +1,14 @@
import { api } from "../convex/_generated/api";
-import { FormEvent } from 'react'
+import { FormEvent } from "react";
-import { useMutation, useQuery } from 'convex/react'
-import { isOpen, hasPlayer } from "../convex/utils"
-import { gameTitle } from "../common"
+import { useMutation, useQuery } from "convex/react";
+import { isOpen, hasPlayer } from "../convex/utils";
+import { gameTitle } from "../common";
-import { useRouter } from 'next/router'
-import { useAuth0 } from '@auth0/auth0-react';
+import { useRouter } from "next/router";
+import { useAuth0 } from "@auth0/auth0-react";
-export default function() {
+export default function () {
const router = useRouter();
const { user } = useAuth0();
@@ -20,8 +20,8 @@ export default function() {
async function newGame(event: FormEvent) {
const value = (event.nativeEvent as any).submitter.defaultValue ?? "";
const white = Boolean(Math.round(Math.random()));
- let player1 : "Me" | "Computer" | null = null;
- let player2 : "Me" | "Computer" | null = null;
+ let player1: "Me" | "Computer" | null = null;
+ let player2: "Me" | "Computer" | null = null;
switch (value) {
case "Play vs another Player":
if (white) {
@@ -45,72 +45,73 @@ export default function() {
break;
}
event.preventDefault();
- const id = await startNewGame({player1, player2});
- router.push({ pathname: `/play/${id}`});
+ const id = await startNewGame({ player1, player2 });
+ router.push({ pathname: `/play/${id}` });
}
async function join(event: FormEvent) {
event.preventDefault();
const gameId = (event.nativeEvent as any).submitter.id ?? "";
- router.push({ pathname: `/play/${gameId}`});
+ router.push({ pathname: `/play/${gameId}` });
}
return (
+ onSubmit={newGame}
+ className="control-form d-flex justify-content-center"
+ >
+
+
+
+
Ongoing Games
- {
- ongoingGames.map((game, i) => (
-
- {gameTitle(game)} |
-
-
- |
-
- ))
- }
+ {ongoingGames.map((game, i) => (
+
+ {gameTitle(game)} |
+
+
+ |
+
+ ))}
- )
+ );
}
diff --git a/pages/play/[id].tsx b/pages/play/[id].tsx
index 742c70b..831303d 100644
--- a/pages/play/[id].tsx
+++ b/pages/play/[id].tsx
@@ -1,17 +1,17 @@
-import { api } from '../../convex/_generated/api';
-import { useRouter } from 'next/router';
-import { Chess, Move } from 'chess.js';
-import { Chessboard } from 'react-chessboard';
+import { api } from "../../convex/_generated/api";
+import { useRouter } from "next/router";
+import { Chess, Move } from "chess.js";
+import { Chessboard } from "react-chessboard";
-import { useMutation, useQuery } from 'convex/react';
-import { Id } from '../../convex/_generated/dataModel';
-import { validateMove, isOpen, playerEquals } from '../../convex/utils';
-import { gameTitle } from '../../common';
-import { useEffect, useState } from 'react';
+import { useMutation, useQuery } from "convex/react";
+import { Id } from "../../convex/_generated/dataModel";
+import { validateMove, isOpen, playerEquals } from "../../convex/utils";
+import { gameTitle } from "../../common";
+import { useEffect, useState } from "react";
export default function () {
const router = useRouter();
- const gameId = router.query.id as Id<'games'>;
+ const gameId = router.query.id as Id<"games">;
const moveIdx = router.query.moveIndex
? Number(router.query.moveIndex)
: undefined;
@@ -29,7 +29,7 @@ export default function () {
const { analysis, moveIndex, move } =
useQuery(
api.games.getAnalysis,
- gameState ? { gameId: gameState._id, moveIndex: selectedMove } : 'skip'
+ gameState ? { gameId: gameState._id, moveIndex: selectedMove } : "skip"
) ?? {};
const performMove = useMutation(api.games.move).withOptimisticUpdate(
@@ -41,7 +41,7 @@ export default function () {
game.move({ from, to });
const newState = { ...state };
newState.pgn = game.pgn();
- console.log('nextState', game.history(), gameId);
+ console.log("nextState", game.history(), gameId);
localStore.setQuery(api.games.get, { id: gameId }, newState);
}
}
@@ -78,7 +78,7 @@ export default function () {
await performMove({ gameId, from: sourceSquare, to: targetSquare });
setSelectedMove(undefined);
} else {
- setMainStyle({ backgroundColor: 'red' });
+ setMainStyle({ backgroundColor: "red" });
setTimeout(() => setMainStyle({}), 50);
try {
await tryPerformMove({ gameId, from: sourceSquare, to: targetSquare });
@@ -95,16 +95,16 @@ export default function () {
blackMove: string;
};
let turns: Turn[] = [];
- let history = game.history().length > 0 ? game.history() : [''];
+ let history = game.history().length > 0 ? game.history() : [""];
while (history.length > 0) {
const whiteMove = history.shift() as string;
- const blackMove = (history.shift() as string) ?? '';
+ const blackMove = (history.shift() as string) ?? "";
turns.push({ num: turns.length + 1, whiteMove, blackMove });
}
const boardOrientation = playerEquals(userId, gameState.player2 as any)
- ? 'black'
- : 'white';
+ ? "black"
+ : "white";
return (
@@ -143,7 +143,7 @@ export default function () {
{Math.floor(moveIndex / 2) + 1}
- {moveIndex % 2 ? 'b' : 'a'}. {move}
+ {moveIndex % 2 ? "b" : "a"}. {move}
|
diff --git a/pages/user/[id].tsx b/pages/user/[id].tsx
index a2cf22a..63969cf 100644
--- a/pages/user/[id].tsx
+++ b/pages/user/[id].tsx
@@ -1,41 +1,41 @@
-import { useRouter } from 'next/router'
+import { useRouter } from "next/router";
-import { useQuery, useMutation } from 'convex/react'
-import { useRef, useState } from 'react'
-import { Id } from '../../convex/_generated/dataModel'
-import { api } from '../../convex/_generated/api'
+import { useQuery, useMutation } from "convex/react";
+import { useRef, useState } from "react";
+import { Id } from "../../convex/_generated/dataModel";
+import { api } from "../../convex/_generated/api";
export default function () {
- const router = useRouter()
- const userId = router.query.id as Id<'users'>
- const user = useQuery(api.users.get, { id: userId }) || null
- const myUserId = useQuery(api.users.getMyUser) ?? null
+ const router = useRouter();
+ const userId = router.query.id as Id<"users">;
+ const user = useQuery(api.users.get, { id: userId }) || null;
+ const myUserId = useQuery(api.users.getMyUser) ?? null;
- const imageInput = useRef(null)
- const [selectedImage, setSelectedImage] = useState(null)
+ const imageInput = useRef(null);
+ const [selectedImage, setSelectedImage] = useState(null);
- const generateUploadUrl = useMutation(api.users.generateUploadUrl)
- const setProfilePic = useMutation(api.users.setProfilePic)
+ const generateUploadUrl = useMutation(api.users.generateUploadUrl);
+ const setProfilePic = useMutation(api.users.setProfilePic);
async function handleSetProfilePic(event: any) {
- event.preventDefault()
- setSelectedImage(null)
- ;(imageInput.current as any).value = ''
+ event.preventDefault();
+ setSelectedImage(null);
+ (imageInput.current as any).value = "";
// Step 1: Get a short-lived upload URL
- const postUrl = await generateUploadUrl()
+ const postUrl = await generateUploadUrl();
// Step 2: POST the file to the URL
const result = await fetch(postUrl, {
- method: 'POST',
- headers: { 'Content-Type': (selectedImage as any).type },
+ method: "POST",
+ headers: { "Content-Type": (selectedImage as any).type },
body: selectedImage,
- })
- const { storageId } = await result.json()
+ });
+ const { storageId } = await result.json();
// Step 3: Save the newly allocated storage id to the messages table
- await setProfilePic(storageId)
+ await setProfilePic(storageId);
}
- console.log()
+ console.log();
return (
@@ -44,9 +44,9 @@ export default function () {
) : (
{user?.name
- ?.split(' ')
+ ?.split(" ")
.map((w) => w.slice(0, 1).toUpperCase())
- .join('')}
+ .join("")}
)}
{user?.name}
@@ -72,5 +72,5 @@ export default function () {
)}
- )
+ );
}
diff --git a/styles/globals.css b/styles/globals.css
index 22f2aa9..3648b58 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -7,7 +7,7 @@
}
body {
- font-family: system-ui, 'Segoe UI', Roboto, 'Helvetica Neue', helvetica,
+ font-family: system-ui, "Segoe UI", Roboto, "Helvetica Neue", helvetica,
sans-serif;
}
@@ -116,7 +116,7 @@ input:not([type]) {
font-size: 16px;
}
-input[type='submit'],
+input[type="submit"],
button {
margin-left: 4px;
background: lightblue;
@@ -127,12 +127,12 @@ button {
background-color: rgb(49, 108, 244);
}
-input[type='submit']:hover,
+input[type="submit"]:hover,
button:hover {
background-color: rgb(41, 93, 207);
}
-input[type='submit']:disabled,
+input[type="submit"]:disabled,
button:disabled {
background-color: rgb(122, 160, 248);
}