diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 749eb45..bd6b4df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,9 @@ This is a community project for my discord, so please join if you want to participate. -You can still contribute without being part of discord, but it would be nice. +## How to Contribute -## Join the community +if you see an issue, leave a comment saying "I want to work on this", then I will assign it to you. -Want to help build on this project? +## Join the Discord -- Join the [WDC Discord](https://discord.gg/N2uEyp7Rfu) +The [WDC Discord](https://discord.gg/N2uEyp7Rfu) is where we are discussing this project. We recommend you join if you want to participate. diff --git a/app/globals.css b/app/globals.css index 4679ba1..c5a40ec 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,6 +4,7 @@ /* To change the theme colors, change the values below or use the "Copy code" button at https://ui.shadcn.com/themes */ + @layer base { :root { --background: 0 0% 100%; @@ -32,27 +33,54 @@ --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; } - + .light { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 222.2 84% 4.9%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --radius: 0.5rem; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + } + .dark { - --background: 20 14.3% 4.1%; - --foreground: 60 9.1% 97.8%; - --card: 20 14.3% 4.1%; - --card-foreground: 60 9.1% 97.8%; - --popover: 20 14.3% 4.1%; - --popover-foreground: 60 9.1% 97.8%; - --primary: 60 9.1% 97.8%; - --primary-foreground: 24 9.8% 10%; - --secondary: 12 6.5% 15.1%; - --secondary-foreground: 60 9.1% 97.8%; - --muted: 12 6.5% 15.1%; - --muted-foreground: 24 5.4% 63.9%; - --accent: 12 6.5% 15.1%; - --accent-foreground: 60 9.1% 97.8%; + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; - --destructive-foreground: 60 9.1% 97.8%; - --border: 12 6.5% 15.1%; - --input: 12 6.5% 15.1%; - --ring: 24 5.7% 82.9%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9; --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; --chart-3: 30 80% 55%; @@ -68,4 +96,7 @@ body { @apply bg-gradient-to-b from-slate-950 to-slate-900 text-foreground; } + .light body { + @apply bg-gradient-to-b from-blue-300 to-blue-100; + } } diff --git a/app/header.tsx b/app/header.tsx index 2be764a..523c4b9 100644 --- a/app/header.tsx +++ b/app/header.tsx @@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"; import { useAuthActions } from "@convex-dev/auth/react"; import { GitHubLogoIcon } from "@radix-ui/react-icons"; import { useConvexAuth } from "convex/react"; +import { ThemeToggle } from "@/components/ThemeToggle"; function SignInWithGitHub() { const { signIn } = useAuthActions(); @@ -25,14 +26,14 @@ export default function Header() { const { isAuthenticated } = useConvexAuth(); return ( -
+
Logo SurviveTheNight
- Synced using Convex + Synced using Convex
- {!isAuthenticated ? ( - - ) : ( - - )} +
+
+ +
+ {!isAuthenticated ? ( + + ) : ( + + )} +
); } diff --git a/components/ThemeToggle.tsx b/components/ThemeToggle.tsx index 87468ef..3c4e681 100644 --- a/components/ThemeToggle.tsx +++ b/components/ThemeToggle.tsx @@ -7,13 +7,16 @@ import { useTheme } from "next-themes"; export function ThemeToggle() { const { theme, setTheme } = useTheme(); return ( - - - - - - - + + {theme == "light" ? ( + + + + ) : ( + + + + )} diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index a4e3ff9..f8c7441 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -18,6 +18,7 @@ import type { import type * as auth from "../auth.js"; import type * as constants from "../constants.js"; import type * as games from "../games.js"; +import type * as gemini from "../gemini.js"; import type * as http from "../http.js"; import type * as init from "../init.js"; import type * as maps from "../maps.js"; @@ -37,6 +38,7 @@ declare const fullApi: ApiFromModules<{ auth: typeof auth; constants: typeof constants; games: typeof games; + gemini: typeof gemini; http: typeof http; init: typeof init; maps: typeof maps; diff --git a/convex/constants.ts b/convex/constants.ts index b03b955..f3009e7 100644 --- a/convex/constants.ts +++ b/convex/constants.ts @@ -3,6 +3,10 @@ export const AI_MODELS = [ model: "gpt-4o", name: "OpenAI - 4o Mini", }, + { + model: "gemini-1.5-pro", + name: "Gemini - 1.5 Pro", + }, ]; export const AI_MODEL_IDS = AI_MODELS.map((model) => model.model); diff --git a/convex/gemini.ts b/convex/gemini.ts new file mode 100644 index 0000000..a83df75 --- /dev/null +++ b/convex/gemini.ts @@ -0,0 +1,140 @@ +import { GoogleGenerativeAI, SchemaType } from "@google/generative-ai"; +import { action, internalAction } from "./_generated/server"; +import { v } from "convex/values"; +import { api, internal } from "./_generated/api"; +import { Doc } from "./_generated/dataModel"; +import { ZombieSurvival } from "../simulators/zombie-survival"; + +const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY as string); + +const schema = { + description: "Game Round Results", + type: SchemaType.OBJECT, + properties: { + map: { + type: SchemaType.ARRAY, + items: { + type: SchemaType.ARRAY, + items: { + type: SchemaType.STRING, + }, + }, + description: "The resulting map after the placements", + }, + reasoning: { + type: SchemaType.STRING, + description: "The reasoning behind the move", + }, + playerCoordinates: { + type: SchemaType.ARRAY, + items: { + type: SchemaType.NUMBER, + }, + description: "The player's coordinates", + }, + boxCoordinates: { + type: SchemaType.ARRAY, + items: { + type: SchemaType.ARRAY, + items: { + type: SchemaType.NUMBER, + }, + }, + description: "The box coordinates", + }, + }, + required: ["map", "reasoning", "playerCoordinates", "boxCoordinates"], +}; + +const model = genAI.getGenerativeModel({ + model: "gemini-1.5-pro", + generationConfig: { + responseMimeType: "application/json", + responseSchema: schema, + }, +}); + +type playMapActionResponse = { + map: string[][]; + reasoning: string; + playerCoordinates: number[]; + boxCoordinates: number[][]; +}; + +export const playMapAction = internalAction({ + args: { + level: v.number(), + gameId: v.id("games"), + modelId: v.string(), + }, + handler: async (ctx, args) => { + const resultId = await ctx.runMutation( + internal.results.createInitialResult, + { + gameId: args.gameId, + level: args.level, + }, + ); + + const map: Doc<"maps"> | null = (await ctx.runQuery( + api.maps.getMapByLevel, + { + level: args.level, + }, + )) as any; + + if (!map) { + throw new Error("Map not found"); + } + + if (process.env.MOCK_GEMINI === "true") { + const existingMap = [...map.grid.map((row) => [...row])]; + existingMap[0][0] = "P"; + existingMap[0][1] = "B"; + existingMap[0][2] = "B"; + return { + map: existingMap, + reasoning: "This is a mock response", + playerCoordinates: [0, 0], + boxCoordinates: [], + }; + } + + const result = await model.generateContent( + `You're given a 2d grid of nums such that. + " " represents an empty space. + "Z" represents a zombie. Zombies move one Manhattan step every turn and aim to reach the player. + "R" represents rocks, which players can shoot over but zombies cannot pass through or break. + "P" represents the player, who cannot move. The player's goal is to shoot and kill zombies before they reach them. + "B" represents blocks that can be placed before the round begins to hinder the zombies. You can place up to two blocks on the map. + + Your goal is to place the player ("P") and two blocks ("B") in locations that maximize the player's survival by delaying the zombies' approach. + You can shoot any zombie regardless of where it is on the grid. + Returning a 2d grid with the player and blocks placed in the optimal locations, with the coordinates player ("P") and the blocks ("B"), also provide reasoning for the choices. + + You can't replace rocks R or zombies Z with blocks. If there is no room to place a block, do not place any. + + Grid: ${JSON.stringify(map)}`, + ); + + // todo: check if the response is valid acc to types and the player and box coordinates are valid, + // as sometimes the model returns a state that's erroring out in the simulator + + const parsedResponse = JSON.parse( + result.response.text(), + ) as playMapActionResponse; + + const game = new ZombieSurvival(parsedResponse.map); + while (!game.finished()) { + game.step(); + } + const isWin = !game.getPlayer().dead(); + + await ctx.runMutation(internal.results.updateResult, { + resultId, + isWin, + reasoning: parsedResponse.reasoning, + map: parsedResponse.map, + }); + }, +}); diff --git a/package.json b/package.json index 8af9657..cd82859 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dependencies": { "@auth/core": "^0.34.2", "@convex-dev/auth": "^0.0.71", + "@google/generative-ai": "^0.21.0", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-select": "^2.1.2", diff --git a/simulators/zombie-survival/Position.ts b/simulators/zombie-survival/Position.ts index 61a08f9..09614ba 100644 --- a/simulators/zombie-survival/Position.ts +++ b/simulators/zombie-survival/Position.ts @@ -2,7 +2,3 @@ export interface Position { x: number; y: number; } - -export function positionAsNumber(position: Position): number { - return position.x + position.y; -} diff --git a/simulators/zombie-survival/ZombieSurvival.spec.ts b/simulators/zombie-survival/ZombieSurvival.spec.ts index 44e0ed8..4851456 100644 --- a/simulators/zombie-survival/ZombieSurvival.spec.ts +++ b/simulators/zombie-survival/ZombieSurvival.spec.ts @@ -360,3 +360,72 @@ test("player gets killed behind walls", () => { expect(game.finished()).toBeTruthy(); }); + +test("player kills zombie and it doesn't hit afterwards", () => { + const game = new ZombieSurvival([ + ["P", " "], + [" ", "Z"], + ]); + + game.step(); + game.step(); + + expect(game.getState()).toStrictEqual([ + ["P", " "], + [" ", " "], + ]); + + expect(game.finished()).toBeTruthy(); +}); + +test.only("player kills closest zombie", () => { + const game = new ZombieSurvival([ + [" ", " ", "R", " ", "Z"], + [" ", " ", " ", " ", "B"], + [" ", "P", " ", "R", " "], + ["Z", " ", " ", " ", " "], + [" ", " ", "B", " ", " "], + ]); + + game.step(); + + expect(game.getState()).toStrictEqual([ + [" ", " ", "R", "Z", " "], + [" ", " ", " ", " ", "B"], + [" ", "P", " ", "R", " "], + [" ", "Z", " ", " ", " "], + [" ", " ", "B", " ", " "], + ]); + + game.step(); + + expect(game.getState()).toStrictEqual([ + [" ", " ", "R", " ", " "], + [" ", " ", " ", "Z", "B"], + [" ", "P", " ", "R", " "], + [" ", " ", " ", " ", " "], + [" ", " ", "B", " ", " "], + ]); + + game.step(); + + expect(game.getState()).toStrictEqual([ + [" ", " ", "R", " ", " "], + [" ", " ", "Z", " ", "B"], + [" ", "P", " ", "R", " "], + [" ", " ", " ", " ", " "], + [" ", " ", "B", " ", " "], + ]); + + game.step(); + + expect(game.getState()).toStrictEqual([ + [" ", " ", "R", " ", " "], + [" ", " ", " ", " ", "B"], + [" ", "P", " ", "R", " "], + [" ", " ", " ", " ", " "], + [" ", " ", "B", " ", " "], + ]); + + expect(game.finished()).toBeTruthy(); +}); diff --git a/simulators/zombie-survival/ZombieSurvival.ts b/simulators/zombie-survival/ZombieSurvival.ts index 5c4a2a1..3b20433 100644 --- a/simulators/zombie-survival/ZombieSurvival.ts +++ b/simulators/zombie-survival/ZombieSurvival.ts @@ -17,6 +17,16 @@ export class ZombieSurvival { return new ZombieSurvival(config); } + public static isWin(config: string[][]): boolean { + const game = new ZombieSurvival(config); + + while (!game.finished()) { + game.step(); + } + + return !game.getPlayer().dead(); + } + public constructor(config: string[][]) { if (config.length === 0 || config[0].length == 0) { throw new Error("Config is empty"); diff --git a/simulators/zombie-survival/lib/closestEntity.ts b/simulators/zombie-survival/lib/closestEntity.ts index 2facd5e..82d2267 100644 --- a/simulators/zombie-survival/lib/closestEntity.ts +++ b/simulators/zombie-survival/lib/closestEntity.ts @@ -1,13 +1,14 @@ import { Entity } from "../entities/Entity"; -import { positionAsNumber } from "../Position"; export interface ClosestEntityScore { - score: number; + distance: number; target: Entity; } export function closestEntity(entity: Entity, targets: Entity[]): Entity { - const entityPosition = positionAsNumber(entity.getPosition()); + const entityPosition = entity.getPosition(); + const x1 = entityPosition.x; + const y1 = entityPosition.y; const scores: ClosestEntityScore[] = []; for (const target of targets) { @@ -15,16 +16,18 @@ export function closestEntity(entity: Entity, targets: Entity[]): Entity { continue; } - const targetPosition = positionAsNumber(target.getPosition()); - const score = Math.abs(entityPosition - targetPosition); + const targetPosition = target.getPosition(); + const x2 = targetPosition.x; + const y2 = targetPosition.y; + const distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); - scores.push({ target, score }); + scores.push({ distance, target }); } if (scores.length === 0) { throw new Error("No alive targets found"); } - scores.sort((a, b) => a.score - b.score); + scores.sort((a, b) => a.distance - b.distance); return scores[0].target; } diff --git a/tailwind.config.ts b/tailwind.config.ts index 25a0c3d..0248b43 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,14 +1,14 @@ import type { Config } from "tailwindcss"; -const config = { - darkMode: "selector", +const config: Config = { + darkMode: "class", // Use 'class' strategy for toggling dark mode content: [ "./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}", ], - safelist: ["dark"], + safelist: ["dark", "light"], // Safelist the 'light' class prefix: "", theme: { container: { @@ -53,6 +53,42 @@ const config = { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, + // Light mode colors + light: { + border: "hsl(var(--light-border))", + input: "hsl(var(--light-input))", + ring: "hsl(var(--light-ring))", + background: "hsl(var(--light-background))", + foreground: "hsl(var(--light-foreground))", + primary: { + DEFAULT: "hsl(var(--light-primary))", + foreground: "hsl(var(--light-primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--light-secondary))", + foreground: "hsl(var(--light-secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--light-destructive))", + foreground: "hsl(var(--light-destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--light-muted))", + foreground: "hsl(var(--light-muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--light-accent))", + foreground: "hsl(var(--light-accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--light-popover))", + foreground: "hsl(var(--light-popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--light-card))", + foreground: "hsl(var(--light-card-foreground))", + }, + }, }, borderRadius: { lg: "var(--radius)", @@ -69,13 +105,8 @@ const config = { to: { height: "0" }, }, }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - }, }, }, - plugins: [require("tailwindcss-animate")], -} satisfies Config; +}; -export default config; +export default config; \ No newline at end of file