index

Challenge - Build and Extend Tamagochi

· 13min

🎮 Tamagotchi Game Setup Guide

🚀 Setup Project

Command:

npx create-react-router@latest <gimme_name>

Command:

npx shadcn@latest init

Command:

npm i @tanstack/react-query

🎨 Install UI Components

Command:

npx shadcn@latest add button

Command:

npx shadcn@latest add dialog

Command:

npx shadcn@latest add input

Command:

npx shadcn@latest add label

Command:

npx shadcn@latest add sonner

Command:

npx shadcn@latest add tooltip

🎯 Install Game Components

Command:

npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/wallet.client.json"

Command:

npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/use-submit-transaction.json"

Command:

npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/use-wallet.json"

Command:

npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/tamago/use-stellar.json"

Command:

npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/tamago/game-context.json"

Command:

npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/tamago/action-button.json"

Command:

npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/tamago/create-pet-dialog.json"

Command:

npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/tamago/header.json"

Command:

npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/tamago/pet-display.json"

Command:

npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/tamago/stats-bar.json"

Command:

npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/tamago/sync-button.json"

Command:

npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/tamago/wallet-connection.json"

Command:

npx shadcn@latest add "https://raw.githubusercontent.com/blockdev-stellar/components/refs/heads/main/tamago/wardrobe.json"

🏗️ Setup Layout

Bikin layout ya ges ya, di /app/routes/_layouts.tsx

Code:

import { Outlet } from "react-router";
import { Header } from "~/components/header";
import { GameProvider } from "~/context/game-context";

export default function () {
  return (
    <GameProvider>
      <div className="min-h-screen flex flex-col bg-background">
        <Header />

        <main className="flex-1 container mx-auto px-4 py-6 sm:py-8">
          <Outlet />
        </main>

        <footer className="border-t-4 border-border bg-card py-4 text-center">
          <p className="text-[10px] sm:text-xs text-muted-foreground">🕹️ A Retro Pixel Pet Game</p>
        </footer>
      </div>
    </GameProvider>
  );
}

🛣️ Configure Routes

Update routes.ts ges

Code:

import { type RouteConfig, index, layout } from "@react-router/dev/routes";

export default [
  layout("routes/_layouts.tsx", [
    index("routes/home.tsx"),
  ]),
] satisfies RouteConfig;

🏠 Build Home Page

Lanjut ke edit routes/home.tsx ya ges ya

Code:

import { PetDisplay } from "~/components/pet-display";
import { StatsBar } from "~/components/stats-bar";
import { Wardrobe } from "~/components/wardrobe";
import { ActionButtons } from "~/components/action-button";
import { useGame } from "~/context/game-context";

...

export default function Home() {
  const { gameState } = useGame();

  return (
    <div className="max-w-2xl mx-auto space-y-6 sm:space-y-8">
      {/* Pet Display */}
      <div className="bg-card border-4 border-border pixel-shadow p-4 sm:p-8">
        <PetDisplay />
      </div>

      {/* Stats Section */}
      <div className="bg-card border-4 border-border pixel-shadow p-4 sm:p-6 space-y-3 sm:space-y-4">
        <h2 className="text-sm sm:text-base font-bold uppercase text-center pixel-text-shadow mb-4">
          Pet Stats
        </h2>
        <StatsBar label="Hunger" value={gameState.stats.hunger} icon="🍖" color="hunger" />
        <StatsBar label="Happy" value={gameState.stats.happy} icon="😊" color="happy" />
        <StatsBar label="Energy" value={gameState.stats.energy} icon="⚡" color="energy" />
      </div>

      {/* Action Buttons */}
      <div className="flex flex-col items-center gap-3">
        <ActionButtons />
        <Wardrobe />
      </div>

      {/* Instructions */}
      <div className="bg-muted border-2 border-border p-4 text-center">
        <p className="text-[10px] sm:text-xs text-muted-foreground leading-relaxed">
          💡 <strong>Tip:</strong> Feed, play, work, and rest to keep your pet happy! Stats decay
          over time. Earn coins from work to unlock cool items. 🎮
        </p>
      </div>
    </div>
  );
}

🎯 Update Root Component

Update root.tsx gaes

Code:

export const links: Route.LinksFunction = () => [
  ...
  {
    rel: "stylesheet",
    href: "https://fonts.googleapis.com/css2?family=Noto+Serif:ital,wght@0,100..900;1,100..900&family=Press+Start+2P&display=swap",
  },
];

...

export default function App() {
  const queryClient = new QueryClient();

  return (
    <QueryClientProvider client={queryClient}>
      <TooltipProvider>
        <Toaster
          position="top-center"
          toastOptions={{
            duration: 2000,
            style: {
              fontFamily: '"Press Start 2P", cursive',
              fontSize: "10px",
              padding: "12px",
              border: "3px solid hsl(var(--border))",
              boxShadow: "4px 4px 0px rgba(0, 0, 0, 0.25)",
            },
          }}
        />
        <Outlet />
      </TooltipProvider>
    </QueryClientProvider>
  );
}
...

🔧 Fix Imports

Fix importnya dengan shortcut:

  • Mac Cmd + .
  • Windows / Linux Ctrl + .

Atau bisa langsung copy paste ini

Code:

import { Toaster } from "~/components/ui/sonner";
import { TooltipProvider } from "~/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

📝 Quick Fixes

Laluuuuuu, find all this word <CONTRACT_ID>, kemudian ganti dengan folder yang kamu generate

Fix import di beberapa component, find all this word ~/app/, kemudian remove part ini saja /app

🎨 Style It Up!

Update app.css

Code:

@import "tailwindcss";
@import "tw-animate-css";

@custom-variant dark (&:is(.dark *));

@theme {
  --font-sans:
    "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
    "Segoe UI Symbol", "Noto Color Emoji";

  --font-pixel: "Press Start 2P", system-ui;
}

html,
body {
  @apply bg-white dark:bg-gray-950;

  @media (prefers-color-scheme: dark) {
    color-scheme: dark;
  }
}

@theme inline {
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  --color-card-foreground: var(--card-foreground);
  --color-popover: var(--popover);
  --color-popover-foreground: var(--popover-foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-secondary: var(--secondary);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
  --color-accent: var(--accent);
  --color-accent-foreground: var(--accent-foreground);
  --color-destructive: var(--destructive);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
  --color-chart-1: var(--chart-1);
  --color-chart-2: var(--chart-2);
  --color-chart-3: var(--chart-3);
  --color-chart-4: var(--chart-4);
  --color-chart-5: var(--chart-5);
  --color-sidebar: var(--sidebar);
  --color-sidebar-foreground: var(--sidebar-foreground);
  --color-sidebar-primary: var(--sidebar-primary);
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
  --color-sidebar-accent: var(--sidebar-accent);
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
  --color-sidebar-border: var(--sidebar-border);
  --color-sidebar-ring: var(--sidebar-ring);
}

:root {
  --radius: 0.25rem;

  /* Retro 8-bit Mario-inspired color palette (converted to OKLCH) */
  --background: oklch(0.93 0.02 85); /* HSL 40 40% 92% */
  --foreground: oklch(0.21 0 0); /* HSL 0 0% 13% */

  --card: oklch(1 0 0); /* HSL 0 0% 100% */
  --card-foreground: oklch(0.21 0 0); /* HSL 0 0% 13% */

  --popover: oklch(1 0 0); /* HSL 0 0% 100% */
  --popover-foreground: oklch(0.21 0 0); /* HSL 0 0% 13% */

  /* Mario Red - Primary action color */
  --primary: oklch(0.58 0.22 25); /* HSL 2 85% 52% */
  --primary-foreground: oklch(1 0 0); /* HSL 0 0% 100% */

  /* Sky Blue - Secondary color */
  --secondary: oklch(0.55 0.17 235); /* HSL 201 100% 43% */
  --secondary-foreground: oklch(1 0 0); /* HSL 0 0% 100% */

  --muted: oklch(0.86 0.02 85); /* HSL 40 20% 82% */
  --muted-foreground: oklch(0.48 0 0); /* HSL 0 0% 40% */

  /* Coin Gold - Accent color */
  --accent: oklch(0.82 0.18 95); /* HSL 48 100% 49% */
  --accent-foreground: oklch(0.21 0 0); /* HSL 0 0% 13% */

  /* Retro Green - Success/Grass */
  --success: oklch(0.52 0.18 155); /* HSL 145 100% 33% */
  --success-foreground: oklch(1 0 0); /* HSL 0 0% 100% */

  --destructive: oklch(0.68 0.24 25); /* HSL 0 84.2% 60.2% */
  --destructive-foreground: oklch(1 0 0); /* HSL 0 0% 100% */

  --border: oklch(0.21 0 0); /* HSL 0 0% 13% */
  --input: oklch(1 0 0); /* HSL 0 0% 100% */
  --ring: oklch(0.58 0.22 25); /* HSL 2 85% 52% */

  /* Retro game-specific tokens */
  --pixel-shadow: 4px 4px 0px oklch(0 0 0 / 0.25);
  --pixel-border: 4px;
  --brick: oklch(0.55 0.18 35); /* HSL 14 100% 45% */
  --coin: oklch(0.82 0.18 95); /* HSL 48 100% 49% */

  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);

  --sidebar-background: oklch(0.98 0 0); /* HSL 0 0% 98% */
  --sidebar: oklch(0.98 0 0);
  --sidebar-foreground: oklch(0.33 0 0); /* HSL 240 5.3% 26.1% */
  --sidebar-primary: oklch(0.18 0 0); /* HSL 240 5.9% 10% */
  --sidebar-primary-foreground: oklch(0.98 0 0); /* HSL 0 0% 98% */
  --sidebar-accent: oklch(0.96 0 0); /* HSL 240 4.8% 95.9% */
  --sidebar-accent-foreground: oklch(0.18 0 0); /* HSL 240 5.9% 10% */
  --sidebar-border: oklch(0.92 0.01 260); /* HSL 220 13% 91% */
  --sidebar-ring: oklch(0.65 0.22 255); /* HSL 217.2 91.2% 59.8% */
}

.dark {
  /* Dark retro theme - night game mode (converted to OKLCH) */
  --background: oklch(0.22 0.02 260); /* HSL 240 20% 12% */
  --foreground: oklch(0.96 0 0); /* HSL 0 0% 95% */

  --card: oklch(0.26 0.02 260); /* HSL 240 15% 16% */
  --card-foreground: oklch(0.96 0 0); /* HSL 0 0% 95% */

  --popover: oklch(0.26 0.02 260); /* HSL 240 15% 16% */
  --popover-foreground: oklch(0.96 0 0); /* HSL 0 0% 95% */

  /* Mario Red - Primary action color */
  --primary: oklch(0.58 0.22 25); /* HSL 2 85% 52% */
  --primary-foreground: oklch(1 0 0); /* HSL 0 0% 100% */

  /* Sky Blue - Secondary color */
  --secondary: oklch(0.55 0.17 235); /* HSL 201 100% 43% */
  --secondary-foreground: oklch(1 0 0); /* HSL 0 0% 100% */

  --muted: oklch(0.32 0.02 260); /* HSL 240 15% 22% */
  --muted-foreground: oklch(0.74 0 0); /* HSL 0 0% 70% */

  /* Coin Gold - Accent color */
  --accent: oklch(0.82 0.18 95); /* HSL 48 100% 49% */
  --accent-foreground: oklch(0.21 0 0); /* HSL 0 0% 13% */

  /* Retro Green - Success/Grass */
  --success: oklch(0.52 0.18 155); /* HSL 145 100% 33% */
  --success-foreground: oklch(1 0 0); /* HSL 0 0% 100% */

  --destructive: oklch(0.62 0.24 25); /* HSL 0 62.8% 50% */
  --destructive-foreground: oklch(1 0 0); /* HSL 0 0% 100% */

  --border: oklch(0.34 0 0); /* HSL 0 0% 25% */
  --input: oklch(0.3 0.02 260); /* HSL 240 15% 20% */
  --ring: oklch(0.58 0.22 25); /* HSL 2 85% 52% */

  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
  --chart-3: oklch(0.769 0.188 70.08);
  --chart-4: oklch(0.627 0.265 303.9);
  --chart-5: oklch(0.645 0.246 16.439);

  --sidebar-background: oklch(0.18 0 0); /* HSL 240 5.9% 10% */
  --sidebar: oklch(0.18 0 0);
  --sidebar-foreground: oklch(0.96 0 0); /* HSL 240 4.8% 95.9% */
  --sidebar-primary: oklch(0.57 0.22 265); /* HSL 224.3 76.3% 48% */
  --sidebar-primary-foreground: oklch(1 0 0); /* HSL 0 0% 100% */
  --sidebar-accent: oklch(0.26 0.02 260); /* HSL 240 3.7% 15.9% */
  --sidebar-accent-foreground: oklch(0.96 0 0); /* HSL 240 4.8% 95.9% */
  --sidebar-border: oklch(0.26 0.02 260); /* HSL 240 3.7% 15.9% */
  --sidebar-ring: oklch(0.65 0.22 255); /* HSL 217.2 91.2% 59.8% */
}

/* @layer base {
  * {
    @apply border-border outline-ring/50;
  }
  body {
    @apply bg-background text-foreground;
  }
} */

@layer base {
  * {
    @apply border-border;
  }

  body {
    @apply bg-background text-foreground font-pixel;
    image-rendering: pixelated;
    image-rendering: -moz-crisp-edges;
    image-rendering: crisp-edges;
  }
}

@layer utilities {
  .pixel-shadow {
    box-shadow: var(--pixel-shadow);
  }

  .pixel-border {
    border: var(--pixel-border) solid oklch(var(--border));
  }

  .pixel-text-shadow {
    text-shadow: 2px 2px 0px oklch(0 0 0 / 0.5);
  }
}

⚙️ Configure Vite

Update vite.config.ts

Code:

export default defineConfig({
  ...
  define: {
    global: "window",
  },
});

🔥 Go Full SPA!

Update juga react-router.config.ts untuk menjadi full SPA, lupakan tentang SSR

Code:

export default {
  ssr: false,
} satisfies Config;

🦀 Smart Contract Time!

Code contract lib.rs

Code:

#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String};

// Define the maximum value for stats
const MAX_STAT: u32 = 100;

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Pet {
    pub owner: Address,
    pub name: String,
    pub birthdate: u64,
    pub last_updated: u64,
    pub is_alive: bool,

    // Stats
    pub hunger: u32,
    pub happiness: u32,
    pub energy: u32,

    // Customization
    pub has_glasses: bool,
}

#[contracttype]
pub enum DataKey {
    Pet(Address),
    Coins(Address),
}

#[contract]
pub struct TamagotchiContract;

#[contractimpl]
impl TamagotchiContract {
    pub fn create(env: Env, owner: Address, name: String) -> Pet {
        owner.require_auth();

        // Check if pet already exists
        if env.storage().instance().has(&DataKey::Pet(owner.clone())) {
            let existing_pet: Pet = env
                .storage()
                .instance()
                .get(&DataKey::Pet(owner.clone()))
                .unwrap();
            if existing_pet.is_alive {
                panic!("Pet already exists for this owner");
            }
        }

        let current_time = env.ledger().timestamp();

        let pet = Pet {
            owner: owner.clone(),
            name: name.clone(),
            birthdate: current_time,
            last_updated: current_time,
            is_alive: true,
            hunger: MAX_STAT,
            happiness: MAX_STAT,
            energy: MAX_STAT,
            has_glasses: false,
        };

        // This will overwrite the old pet data if it exists
        env.storage()
            .instance()
            .set(&DataKey::Pet(owner.clone()), &pet);

        // Initialize coins for new pet (reset to 0 if recreating)
        env.storage().instance().set(&DataKey::Coins(owner), &0i128);

        pet
    }

    pub fn feed(env: Env, owner: Address) {
        owner.require_auth();
        let mut pet = Self::get_pet(env.clone(), owner.clone());
        if !pet.is_alive {
            panic!("Your pet is no longer with us.");
        }

        // Apply action
        pet.hunger = (pet.hunger + 30).min(MAX_STAT);

        // Update state
        pet.last_updated = env.ledger().timestamp();
        env.storage().instance().set(&DataKey::Pet(owner), &pet);
    }

    pub fn play(env: Env, owner: Address) {
        owner.require_auth();
        let mut pet = Self::get_pet(env.clone(), owner.clone());
        if !pet.is_alive {
            panic!("Your pet is no longer with us.");
        }

        // Apply action
        pet.happiness = (pet.happiness + 20).min(MAX_STAT);
        pet.energy = pet.energy.saturating_sub(15);

        // Update state
        pet.last_updated = env.ledger().timestamp();
        env.storage().instance().set(&DataKey::Pet(owner), &pet);
    }

    pub fn sleep(env: Env, owner: Address) {
        owner.require_auth();
        let mut pet = Self::get_pet(env.clone(), owner.clone());
        if !pet.is_alive {
            panic!("Your pet is no longer with us.");
        }

        // Apply action
        pet.energy = (pet.energy + 40).min(MAX_STAT);

        // Update state
        pet.last_updated = env.ledger().timestamp();
        env.storage().instance().set(&DataKey::Pet(owner), &pet);
    }

    pub fn work(env: Env, owner: Address) {
        owner.require_auth();
        let mut pet = Self::get_pet(env.clone(), owner.clone());
        if !pet.is_alive {
            panic!("Your pet is no longer with us.");
        }

        if pet.energy < 20 {
            panic!("Not enough energy to work.");
        }

        // Apply action
        pet.energy = pet.energy.saturating_sub(20);
        pet.happiness = pet.happiness.saturating_sub(10);

        // Grant coins
        let mut coins: i128 = env
            .storage()
            .instance()
            .get(&DataKey::Coins(owner.clone()))
            .unwrap_or(0);
        coins += 25;

        // Update state
        pet.last_updated = env.ledger().timestamp();
        env.storage()
            .instance()
            .set(&DataKey::Pet(owner.clone()), &pet);
        env.storage().instance().set(&DataKey::Coins(owner), &coins);
    }

    pub fn mint_glasses(env: Env, owner: Address) {
        owner.require_auth();
        let mut pet = Self::get_pet(env.clone(), owner.clone());
        if !pet.is_alive {
            panic!("Your pet is no longer with us.");
        }

        let mut coins: i128 = env
            .storage()
            .instance()
            .get(&DataKey::Coins(owner.clone()))
            .unwrap_or(0);
        if coins < 50 {
            panic!("Not enough coins to mint glasses.");
        }
        coins -= 50;

        pet.has_glasses = true;

        pet.last_updated = env.ledger().timestamp();
        env.storage()
            .instance()
            .set(&DataKey::Pet(owner.clone()), &pet);
        env.storage().instance().set(&DataKey::Coins(owner), &coins);
    }

    pub fn get_pet(env: Env, owner: Address) -> Pet {
        let mut pet: Pet = env
            .storage()
            .instance()
            .get(&DataKey::Pet(owner.clone()))
            .expect("Pet not found");

        if !pet.is_alive {
            return pet;
        }

        let current_time = env.ledger().timestamp();
        let time_elapsed = current_time.saturating_sub(pet.last_updated);

        // Stats decay every 60 seconds
        let decay_periods = time_elapsed / 60;

        if decay_periods > 0 {
            // Hunger decays by 2 points per period
            pet.hunger = pet.hunger.saturating_sub((decay_periods as u32) * 2);
            // Happiness decays by 1 point per period
            pet.happiness = pet.happiness.saturating_sub((decay_periods as u32) * 1);

            pet.last_updated = current_time;

            if pet.hunger == 0 || pet.happiness == 0 {
                pet.is_alive = false;
            }

            env.storage().instance().set(&DataKey::Pet(owner), &pet);
        }

        pet
    }

    pub fn get_coins(env: Env, owner: Address) -> i128 {
        env.storage()
            .instance()
            .get(&DataKey::Coins(owner))
            .unwrap_or(0)
    }
}

#[cfg(test)]
mod test;

🧪 Test Contract

Code test.rs

Code:

#![cfg(test)]

use super::{TamagotchiContract, TamagotchiContractClient, MAX_STAT};
use soroban_sdk::{
    testutils::{Address as _, Ledger, LedgerInfo},
    Address, Env, String,
};

fn create_tamagotchi_contract(env: &Env) -> TamagotchiContractClient<'_> {
    TamagotchiContractClient::new(env, &env.register(TamagotchiContract, ()))
}

fn advance_ledger(env: &Env, seconds: u64) {
    env.ledger().set(LedgerInfo {
        timestamp: env.ledger().timestamp() + seconds,
        protocol_version: env.ledger().protocol_version(),
        sequence_number: env.ledger().sequence(),
        network_id: Default::default(),
        base_reserve: 10,
        min_temp_entry_ttl: 1,
        min_persistent_entry_ttl: 1,
        max_entry_ttl: u32::MAX,
    });
}

#[test]
fn test_create_pet() {
    let env = Env::default();
    env.mock_all_auths();
    let client = create_tamagotchi_contract(&env);
    let owner = Address::generate(&env);
    let name = String::from_str(&env, "Pixel");

    let pet = client.create(&owner, &name);

    assert_eq!(pet.owner, owner);
    assert_eq!(pet.name, name);
    assert_eq!(pet.is_alive, true);
    assert_eq!(pet.hunger, MAX_STAT);
    assert_eq!(pet.happiness, MAX_STAT);
    assert_eq!(pet.energy, MAX_STAT);
    assert_eq!(pet.has_glasses, false);

    let coins = client.get_coins(&owner);
    assert_eq!(coins, 0);
}

#[test]
#[should_panic(expected = "Pet already exists for this owner")]
fn test_create_pet_already_exists() {
    let env = Env::default();
    env.mock_all_auths();
    let client = create_tamagotchi_contract(&env);
    let owner = Address::generate(&env);
    let name = String::from_str(&env, "Pixel");

    client.create(&owner, &name);
    client.create(&owner, &name); // Should panic here
}

#[test]
fn test_feed() {
    let env = Env::default();
    env.mock_all_auths();
    let client = create_tamagotchi_contract(&env);
    let owner = Address::generate(&env);

    client.create(&owner, &String::from_str(&env, "Giga"));

    // Let's simulate decay first.
    advance_ledger(&env, 60 * 5); // 5 minutes
    client.feed(&owner);
    let pet = client.get_pet(&owner);

    // Initial: 100. Decay over 5 mins (5 periods): 100 - (5*2) = 90.
    // Feed: 90 + 30 = 120, capped at 100.
    assert_eq!(pet.hunger, MAX_STAT);
}

#[test]
fn test_play() {
    let env = Env::default();
    env.mock_all_auths();
    let client = create_tamagotchi_contract(&env);
    let owner = Address::generate(&env);
    client.create(&owner, &String::from_str(&env, "Player"));

    advance_ledger(&env, 60 * 10); // 10 minutes
                                   // Initial happiness: 100. Decay: 100 - (10*1) = 90.
                                   // Initial energy: 100. No decay for energy.

    client.play(&owner);
    let pet = client.get_pet(&owner);

    // Happiness: 90 + 20 = 110, capped at 100.
    // Energy: 100 - 15 = 85.
    assert_eq!(pet.happiness, MAX_STAT);
    assert_eq!(pet.energy, 85);
}

#[test]
fn test_work() {
    let env = Env::default();
    env.mock_all_auths();
    let client = create_tamagotchi_contract(&env);
    let owner = Address::generate(&env);
    client.create(&owner, &String::from_str(&env, "Worker"));

    client.work(&owner);
    let pet = client.get_pet(&owner);
    let coins = client.get_coins(&owner);

    // Energy: 100 - 20 = 80
    // Happiness: 100 - 10 = 90
    // Coins: 0 + 25 = 25
    assert_eq!(pet.energy, 80);
    assert_eq!(pet.happiness, 90);
    assert_eq!(coins, 25);
}

#[test]
#[should_panic(expected = "Not enough energy to work.")]
fn test_work_no_energy() {
    let env = Env::default();
    env.mock_all_auths();
    let client = create_tamagotchi_contract(&env);
    let owner = Address::generate(&env);
    client.create(&owner, &String::from_str(&env, "Tired"));

    // Work 5 times to drain energy
    for _ in 0..5 {
        client.work(&owner);
    }
    // Energy should be 0 now.
    let pet = client.get_pet(&owner);
    assert_eq!(pet.energy, 0);

    client.work(&owner); // Should panic
}

#[test]
fn test_sleep() {
    let env = Env::default();
    env.mock_all_auths();
    let client = create_tamagotchi_contract(&env);
    let owner = Address::generate(&env);
    client.create(&owner, &String::from_str(&env, "Sleepy"));

    // Work to reduce energy
    client.work(&owner); // Energy: 80
    client.work(&owner); // Energy: 60

    client.sleep(&owner);
    let pet = client.get_pet(&owner);
    // Energy: 60 + 40 = 100
    assert_eq!(pet.energy, MAX_STAT);
}

#[test]
fn test_stat_decay_and_death() {
    let env = Env::default();
    env.mock_all_auths();
    let client = create_tamagotchi_contract(&env);
    let owner = Address::generate(&env);
    client.create(&owner, &String::from_str(&env, "Doomed"));

    // Hunger decays by 2 every 60s. It needs 50 periods to reach 0.
    // 50 periods * 60s/period = 3000 seconds.
    advance_ledger(&env, 3000);

    // Calling any function will trigger the decay calculation.
    let pet = client.get_pet(&owner);
    assert_eq!(pet.is_alive, false);
    assert_eq!(pet.hunger, 0);
}

#[test]
#[should_panic(expected = "Your pet is no longer with us.")]
fn test_action_on_dead_pet() {
    let env = Env::default();
    env.mock_all_auths();
    let client = create_tamagotchi_contract(&env);
    let owner = Address::generate(&env);
    client.create(&owner, &String::from_str(&env, "Ghost"));

    // Advance time enough to kill the pet
    advance_ledger(&env, 3000);

    // This call will update the state to dead
    let pet = client.get_pet(&owner);
    assert_eq!(pet.is_alive, false);

    // This should panic
    client.feed(&owner);
}

#[test]
fn test_mint_glasses() {
    let env = Env::default();
    env.mock_all_auths();
    let client = create_tamagotchi_contract(&env);
    let owner = Address::generate(&env);
    client.create(&owner, &String::from_str(&env, "Cool"));

    // Work to earn coins (need at least 50 for glasses)
    client.work(&owner); // 25 coins
    client.work(&owner); // 50 coins

    client.mint_glasses(&owner);

    let pet = client.get_pet(&owner);
    let coins = client.get_coins(&owner);

    assert_eq!(pet.has_glasses, true);
    assert_eq!(coins, 0);
}

#[test]
#[should_panic(expected = "Not enough coins to mint glasses.")]
fn test_mint_glasses_no_coins() {
    let env = Env::default();
    env.mock_all_auths();
    let client = create_tamagotchi_contract(&env);
    let owner = Address::generate(&env);
    client.create(&owner, &String::from_str(&env, "Broke"));

    // Try to mint glasses without earning any coins
    client.mint_glasses(&owner); // Should panic
}