Challenge - Build and Extend Tamagochi
🎮 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
}