Skip to content

Instantly share code, notes, and snippets.

@Shuyib
Created January 24, 2025 06:53
Show Gist options
  • Save Shuyib/7f7b61d13c7f102d494c5472e58a40cc to your computer and use it in GitHub Desktop.
Save Shuyib/7f7b61d13c7f102d494c5472e58a40cc to your computer and use it in GitHub Desktop.
Pokèmon Battle Advisor

Pokémon Battle Advisor try here https://shuyib-pokemonbattleadvisor.web.val.run

Overview

The Pokémon Battle Advisor is a web application designed to help Pokémon trainers strategize their battles by providing detailed and realistic battle recommendations. Users can specify their Pokémon team or randomize a team, and then receive tailored battle strategies based on their team and the opponent's Pokémon. The application leverages the CEREBRAS API to generate these recommendations, ensuring they adhere to official Pokémon game mechanics.

Features

  • Specify Your Pokémon Team: Users can enter the names of up to three Pokémon to include in their team.
  • Randomize Pokémon Team: Users can opt to randomize their Pokémon team, fetching three random Pokémon from the PokéAPI.
  • Challenger Pokémon Input: Users can enter the name of the opponent's Pokémon to receive a battle recommendation.
  • Battle Recommendations: The app provides detailed battle strategies, including recommended Pokémon, strategies, and type advantages.
  • Data Validation: Ensures that only valid Pokémon names are accepted, preventing random or incorrect names from being processed.
  • Caching Mechanism: Uses in-memory caching to store fetched Pokémon data, reducing redundant API calls and improving performance.
  • Responsive Design: The app is designed to be responsive, ensuring usability across various devices and screen sizes.

Installation

Prerequisites

  • Deno: This project is built using Deno, a secure runtime for JavaScript and TypeScript.

Steps

  1. Fork the Repository:

  2. Set Up Environment Variables: Create a .env file in the root directory and add your Cerebras API key:

    CEREBRAS_API_KEY=your_cerebras_api_key

    Note: Ensure that your Cerebras API key is kept secure and not exposed publicly.

  3. Run the Application: Execute the following command to start the server:

    deno run --allow-net --allow-env cerebras_try.ts
    • --allow-net: Grants network access needed for API calls.
    • --allow-env: Grants access to environment variables for the API key.
  4. Access the Application: Open your browser and navigate to http://localhost:8000 to view the Pokémon Battle Advisor.

Usage

Specify Your Pokémon:

  • Enter the names of up to three Pokémon in the input fields provided.
  • Click the "🎯 Load Specified Pokémon" button to fetch and display the specified Pokémon.

Randomize Pokémon:

  • Click the "🔀 Randomize Pokémon" button to fetch and display three random Pokémon.

Enter Challenger's Pokémon:

  • Enter the name of the opponent's Pokémon in the input field provided.
  • Click the "🤖 Get Battle Recommendation" button to generate a battle strategy.

View Battle Recommendation:

  • The app will display a detailed battle recommendation, including recommended Pokémon, strategies, and type advantages.

Code Structure

  • cerebras_try.ts: The main TypeScript file that serves both the server and client-side code.

    Server-Side:

    • Handles HTTP requests.
    • Serves the HTML page containing the React application.
    • Provides an API endpoint (/api/cerebras-key) to securely retrieve the Cerebras API key.

    Client-Side:

    • Built using React.
    • Manages the user interface, state management, and interactions.
    • Fetches Pokémon data from the PokéAPI.
    • Communicates with the OpenAI API but uses the CEREBRAS API to generate battle recommendations.

    Components & Functions:

    • App Component: The main React component that manages the state and logic for the application.
    • validatePokemonData Function: Validates the structure and types of fetched Pokémon data.
    • loadUserPokemons Function: Fetches and validates user-specified Pokémon.
    • fetchRandomPokemon Function: Fetches and validates random Pokémon.
    • generateBattleRecommendation Function: Generates battle recommendations using the OpenAI API.
    • formatRecommendation Function: Formats the recommendation text into structured sections.

Error Handling

  • Invalid Pokémon Names: If a user enters an invalid Pokémon name, an error message is displayed prompting the user to enter a valid name.
  • API Errors: Any network or unexpected errors during API calls are caught, and corresponding error messages are displayed to the user.
  • Missing API Key: The application checks for the presence of the Cerebras API key and notifies the user if it's missing or invalid.

Technologies Used

  • Deno: A secure runtime for JavaScript and TypeScript.
  • React: For building the user interface.
  • OpenAI API: For generating battle recommendations.
  • PokéAPI: For fetching Pokémon data.
  • TypeScript: For type safety and improved developer experience.

Contributing

Contributions are welcome! Please follow these steps to contribute:

Relevant when I move it to github.

  1. Fork the Repository: Click the "Fork" button at the top right of the repository page to create a copy of the repository under your GitHub account.

  2. Clone Your Fork:

    git clone https://github.com/yourusername/pokemon-battle-advisor.git
    cd pokemon-battle-advisor
  3. Create a New Branch:

    git checkout -b feature/your-feature-name
  4. Make Your Changes: Implement your feature or bug fix.

  5. Commit Your Changes:

    git commit -m "Add your commit message here"
  6. Push to Your Fork:

    git push origin feature/your-feature-name
  7. Submit a Pull Request: Go to the original repository on GitHub and create a pull request from your forked repository.

License

This project is licensed under the MIT License. See the LICENSE file for details.

Contact

For any questions or feedback, please contact [email protected]

/** @jsxImportSource https://esm.sh/[email protected] */
import OpenAI from "https://esm.sh/[email protected]";
import { createRoot } from "https://esm.sh/[email protected]/client";
import React, { useEffect, useRef, useState } from "https://esm.sh/[email protected]";
interface Pokemon {
id: number;
name: string;
sprites: { front_default: string };
stats: Array<{ base_stat: number; stat: { name: string } }>;
types: Array<{ type: { name: string } }>;
}
// Server-side API key retrieval function
async function getCerebrasKey() {
try {
const response = await fetch("/api/cerebras-key");
const data = await response.json();
return data.apiKey;
} catch (error) {
console.error("Failed to retrieve Cerebras API key:", error);
return null;
}
}
// Utility function to parse and format recommendation
function formatRecommendation(text: string) {
// Split recommendation into sections based on emojis
const sections = text.split(/🏆|🔍|⚔️/).filter(Boolean);
const sectionTitles = text.match(/🏆|🔍|⚔️/g) || [];
return sections.map((section, index) => (
<div key={index} style={{ marginBottom: "15px" }}>
<h3 style={{ display: "flex", alignItems: "center", color: "#2c3e50" }}>
<span style={{ marginRight: "10px" }}>{sectionTitles[index]}</span>
{index === 0 && "Recommended Pokémon"}
{index === 1 && "Strategy"}
{index === 2 && "Type Advantages"}
</h3>
<p style={{ lineHeight: "1.6", color: "#34495e" }}>{section.trim()}</p>
</div>
));
}
function App() {
const [pokemons, setPokemons] = useState<Pokemon[]>([]);
const [battleRecommendation, setBattleRecommendation] = useState<string>("");
const [challengerInput, setChallengerInput] = useState<string>("");
const [error, setError] = useState<string | null>(null);
const [apiKey, setApiKey] = useState<string | null>(null);
// State for user-specified Pokémon inputs
const [userPokemonInputs, setUserPokemonInputs] = useState<string[]>(["", "", ""]);
// Initialize cache using useRef
const pokemonCache = useRef<{ [key: number]: Pokemon }>({});
// Data Validation Function
function validatePokemonData(pokemon: any): Pokemon | null {
if (
typeof pokemon.id !== "number"
|| typeof pokemon.name !== "string"
|| !pokemon.sprites?.front_default
|| !Array.isArray(pokemon.stats)
|| !Array.isArray(pokemon.types)
) {
console.warn("Invalid Pokémon data:", pokemon);
return null;
}
// Further validation can be added here (e.g., checking stats ranges)
return pokemon as Pokemon;
}
// Handle input change for user-specified Pokémon
function handleUserInputChange(index: number, value: string) {
const newInputs = [...userPokemonInputs];
newInputs[index] = value;
setUserPokemonInputs(newInputs);
}
// Load user-specified Pokémon with verification
async function loadUserPokemons() {
const fetchedPokemons: Pokemon[] = [];
for (const name of userPokemonInputs) {
if (name.trim() === "") continue;
try {
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${name.toLowerCase()}`);
if (!response.ok) {
setError(`Pokémon "${name}" not found. Please check the name and try again.`);
return;
}
const data = await response.json();
const validatedPokemon = validatePokemonData(data);
if (validatedPokemon) {
fetchedPokemons.push(validatedPokemon);
// Cache the valid Pokémon
pokemonCache.current[validatedPokemon.id] = validatedPokemon;
} else {
setError(`Invalid data for Pokémon: ${name}`);
return;
}
} catch (err) {
console.error(`Error fetching Pokémon ${name}:`, err);
setError(`Failed to fetch Pokémon: ${name}. Please try again.`);
return;
}
}
if (fetchedPokemons.length > 0) {
setPokemons(fetchedPokemons);
setError(null);
} else {
setError("No valid Pokémon names entered.");
}
}
// Fetch random Pokémon with verification
async function fetchRandomPokemon() {
const pokemonIds = Array.from({ length: 3 }, () => Math.floor(Math.random() * 898) + 1);
const fetchedPokemons: Pokemon[] = [];
for (const id of pokemonIds) {
// Check cache first
if (pokemonCache.current[id]) {
fetchedPokemons.push(pokemonCache.current[id]);
} else {
try {
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
if (!res.ok) {
setError("Failed to fetch a valid Pokémon. Please try again.");
return;
}
const data = await res.json();
const validatedPokemon = validatePokemonData(data);
if (validatedPokemon) {
pokemonCache.current[id] = validatedPokemon; // Cache the valid Pokémon
fetchedPokemons.push(validatedPokemon);
} else {
setError("Fetched Pokémon data is invalid. Please try again.");
return;
}
} catch (err) {
console.error(`Error fetching Pokémon with ID ${id}:`, err);
setError("Failed to fetch Pokémon data. Please try again.");
return;
}
}
}
setPokemons(fetchedPokemons);
// Reset any previous errors when fetching new Pokémon
setError(null);
}
useEffect(() => {
// Initially load random Pokémon
fetchRandomPokemon();
// Fetch API key when component mounts
getCerebrasKey().then(key => {
if (key) {
setApiKey(key);
} else {
setError("Failed to retrieve Cerebras API key");
}
});
}, []);
async function generateBattleRecommendation() {
// Clear previous recommendation and errors
setBattleRecommendation("");
setError(null);
// Validate inputs
if (pokemons.length === 0) {
setError("Please generate or specify Pokémon first");
return;
}
if (!challengerInput.trim()) {
setError("Please enter a challenger's Pokémon");
return;
}
if (!apiKey) {
setError("Cerebras API key is not available");
return;
}
try {
const { OpenAI } = await import("https://esm.sh/openai");
const client = new OpenAI({
apiKey: apiKey,
baseURL: "https://api.cerebras.ai/v1",
dangerouslyAllowBrowser: true, // Added to allow browser-based API calls
});
const recommendation = await client.chat.completions.create({
model: "llama-3.3-70b",
messages: [
{
role: "system",
content: `
You are an expert Pokémon battle strategist familiar with all official Pokémon game mechanics, types, stats, abilities, and move sets. Provide detailed and realistic battle recommendations that adhere to the actual rules and dynamics of Pokémon battles.
Ensure that all suggestions are viable within the context of the Pokémon games and do not include overpowered or non-existent strategies.
Format your responses with the following sections using these prefixes:
🏆 Recommended Pokémon:
🔍 Strategy:
⚔️ Type Advantages:
`,
},
{
role: "user",
content: `Comprehensive battle strategy analysis:
My Team: ${
pokemons.map(p =>
`${p.name.toUpperCase()} (${p.types.map(t => t.type.name.toUpperCase()).join("/")})
Stats - HP: ${p.stats.find(s => s.stat.name === "hp")?.base_stat},
Attack: ${p.stats.find(s => s.stat.name === "attack")?.base_stat},
Defense: ${p.stats.find(s => s.stat.name === "defense")?.base_stat}`
).join(", ")
}
Challenger's Pokémon: ${challengerInput.toUpperCase()}
Provide a strategic battle recommendation that includes:
1. Type advantages and disadvantages
2. Recommended Pokémon from my team
3. Potential battle strategies
4. Specific move suggestions
5. Defensive and offensive considerations`,
},
],
max_tokens: 1500,
});
const recommendationText = recommendation.choices[0]?.message?.content;
if (recommendationText) {
setBattleRecommendation(recommendationText);
} else {
setError("No recommendation could be generated. Please try again.");
}
} catch (error) {
console.error("Battle recommendation error:", error);
setError(`Failed to generate recommendation: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
return (
<div
style={{
fontFamily: "Arial, sans-serif",
maxWidth: "800px",
margin: "auto",
padding: "20px",
boxSizing: "border-box",
width: "100%",
}}
>
<h1 style={{ textAlign: "center", color: "#2c3e50" }}>🎮 Pokémon Battle Advisor 🃏</h1>
<p style={{ textAlign: "center", color: "#7f8c8d" }}>
Get tailored battle strategies by specifying your Pokémon or randomizing them!
</p>
{/* User-Specified Pokémon Inputs */}
<div style={{ marginTop: "20px" }}>
<h2>🔧 Specify Your Pokémon</h2>
<p style={{ color: "#7f8c8d" }}>Enter the names of up to three Pokémon to include in your team.</p>
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
{userPokemonInputs.map((input, index) => (
<input
key={index}
type="text"
value={input}
onChange={(e) => handleUserInputChange(index, e.target.value)}
placeholder={`Enter Pokémon ${index + 1} Name`}
style={{
width: "100%",
padding: "10px",
border: "1px solid #ccc",
borderRadius: "5px",
boxSizing: "border-box",
}}
title={`Enter the name of your Pokémon ${index + 1}`}
/>
))}
<button
onClick={loadUserPokemons}
style={{
padding: "10px 20px",
backgroundColor: "#8e44ad",
color: "white",
border: "none",
borderRadius: "5px",
cursor: "pointer",
transition: "background-color 0.3s",
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLButtonElement).style.backgroundColor = "#732d91";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLButtonElement).style.backgroundColor = "#8e44ad";
}}
title="Load your specified Pokémon into the team"
>
🎯 Load Specified Pokémon
</button>
</div>
</div>
{/* Or */}
<div style={{ textAlign: "center", margin: "20px 0", color: "#7f8c8d" }}>
<span>— OR —</span>
</div>
{/* Randomize Pokémon */}
<div style={{ textAlign: "center" }}>
<button
onClick={fetchRandomPokemon}
style={{
padding: "10px 20px",
backgroundColor: "#4CAF50",
color: "white",
border: "none",
borderRadius: "5px",
cursor: "pointer",
transition: "background-color 0.3s",
width: "100%",
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLButtonElement).style.backgroundColor = "#45a049";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLButtonElement).style.backgroundColor = "#4CAF50";
}}
title="Fetch a random set of Pokémon for your team"
>
🔀 Randomize Pokémon
</button>
</div>
{/* Pokémon Display Grid */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
gap: "10px",
marginTop: "20px",
}}
>
{pokemons.map((pokemon, index) => (
<div
key={pokemon.id}
style={{
border: "1px solid #ddd",
borderRadius: "8px",
padding: "10px",
textAlign: "center",
backgroundColor: "#f9f9f9",
transition: "transform 0.2s",
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLDivElement).style.transform = "scale(1.05)";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLDivElement).style.transform = "scale(1)";
}}
>
<img
src={pokemon.sprites.front_default}
alt={pokemon.name}
style={{ width: "100%", maxHeight: "150px" }}
/>
<h3 style={{ color: "#3498db" }}>{pokemon.name.toUpperCase()}</h3>
<p>Type: {pokemon.types.map(t => t.type.name).join("/")}</p>
<div>
{pokemon.stats.map(stat => (
<div key={stat.stat.name}>
<strong>{stat.stat.name.toUpperCase()}:</strong> {stat.base_stat}
</div>
))}
</div>
</div>
))}
</div>
{/* Challenger Input */}
<div style={{ marginTop: "20px" }}>
<h2>🗡️ Enter Challenger's Pokémon</h2>
<p style={{ color: "#7f8c8d" }}>
Provide the name of your opponent's Pokémon to receive a battle recommendation.
</p>
<input
type="text"
value={challengerInput}
onChange={(e) => setChallengerInput(e.target.value)}
placeholder="Enter challenger's Pokémon"
style={{
width: "100%",
padding: "10px",
marginBottom: "10px",
border: "1px solid #ccc",
borderRadius: "5px",
boxSizing: "border-box",
}}
title="Enter the name of your challenger's Pokémon"
/>
<button
onClick={generateBattleRecommendation}
style={{
padding: "10px 20px",
backgroundColor: "#2196F3",
color: "white",
border: "none",
borderRadius: "5px",
cursor: "pointer",
transition: "background-color 0.3s",
width: "100%",
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLButtonElement).style.backgroundColor = "#1976D2";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLButtonElement).style.backgroundColor = "#2196F3";
}}
title="Generate a battle recommendation based on your team and the challenger"
>
🤖 Get Battle Recommendation
</button>
</div>
{/* Error Message */}
{error && (
<div
style={{
marginTop: "20px",
padding: "15px",
backgroundColor: "#ffdddd",
borderRadius: "8px",
color: "#ff0000",
}}
>
<h3>⚠️ Error:</h3>
<p>{error}</p>
</div>
)}
{/* Battle Recommendation Display */}
{battleRecommendation && (
<div
style={{
marginTop: "20px",
padding: "15px",
backgroundColor: "#ecf0f1",
borderRadius: "8px",
boxShadow: "0 4px 6px rgba(0,0,0,0.1)",
}}
>
<h3
style={{
color: "#2c3e50",
borderBottom: "2px solid #3498db",
paddingBottom: "10px",
display: "flex",
alignItems: "center",
}}
>
🏆 Battle Recommendation
</h3>
<div
style={{
backgroundColor: "white",
padding: "15px",
borderRadius: "5px",
maxHeight: "400px",
overflowY: "auto",
}}
>
{formatRecommendation(battleRecommendation)}
</div>
</div>
)}
{/* View Source Link */}
<a
href={import.meta.url.replace("esm.town", "val.town")}
target="_top"
style={{
display: "block",
marginTop: "20px",
color: "#888",
textDecoration: "none",
}}
>
View Source
</a>
</div>
);
}
function client() {
createRoot(document.getElementById("root")).render(<App />);
}
if (typeof document !== "undefined") { client(); }
export default async function server(request: Request): Promise<Response> {
// Check if this is a request for the API key
const url = new URL(request.url);
if (url.pathname === "/api/cerebras-key") {
const apiKey = Deno.env.get("CEREBRAS_API_KEY");
return new Response(JSON.stringify({ apiKey }), {
headers: { "Content-Type": "application/json" },
});
}
// Regular HTML response
return new Response(
`
<html>
<head>
<title>Pokémon Battle Advisor</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="root"></div>
<script src="https://esm.town/v/std/catch"></script>
<script type="module" src="${import.meta.url}"></script>
</body>
</html>
`,
{
headers: { "content-type": "text/html" },
},
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment