# Project export: Sheepify

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 12.0
- Tagline: Sheepify gamifies sleep: log quality rest, earn virtual sheep that produce wool to unlock rewards. A cozy, pixel-art farm powered by healthy habits and a FastAPI + React Native stack.
- Devpost: https://devpost.com/software/sheepify
- GitHub: https://github.com/kstepanova29/sheepify
- Video: https://www.youtube.com/embed/js1G6lpo18M?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Claude (72 commits), Kevin (12 commits), kstepanova29 (3 commits)

## Devpost submission (written by the team)

### Inspiration

The idea for Sheepify came to us naturally during our first evening at the hackathon: all of us exhausted, venting about college stress, sleepless nights, and the long flights some teammates took just to get to San Francisco. We realized how normalized burnout has become and how rarely rest is celebrated. So we thought: in a world where good sleep is undervalued, why not build something that rewards it? In a world obsessed with productivity, we wanted to flip the script and build something that rewards slowing down – something warm, pixelated, and peaceful.

### What it does

Sheepify gamifies healthy sleep habits. Each time you log a good night’s rest, you earn virtual sheep that produce wool over time – your in-game currency. The better your sleep quality (based on duration and consistency), the more rewards you collect. You can spend wool to personalize your farm, unlock new sheep types, and grow your cozy little dream world. In future updates, we plan to introduce a friends leaderboard, so for once, people can compete over who sleeps best, not who sleeps least. Powered by ClaudeAI, Sheepify also features Sheepy – your cheeky sheep companion who summarizes your dreams, congratulates you for sleeping well, and lovingly roasts you when you stay up too late.

### How we built it

We built Sheepify with React Native and Expo for the mobile frontend, FastAPI for the backend, and SQLite for data storage. Our frontend uses TypeScript and Zustand for smooth state management, while the backend handles user profiles, sleep scoring, and wool generation logic. The visuals were a big part of the process — we designed custom pixel art sprites, animated them frame by frame, and created a consistent 8-bit interface using a purple-themed palette (#3d1b6b) and retro fonts like Press Start 2P.

### Challenges we ran into

This was the first hackathon for everyone on our team – and none of us had ever touched React Native or built a mobile app before. It was a huge learning leap, but we embraced the messy, beautiful process of debugging and figuring things out as we went. Some features we were really excited about didn’t quite make it to the finish line — like our 2-frame sheep animations, which kept overloading the app and causing hilarious amounts of lag. Still, we learned a ton from those hiccups and can’t wait to get them running smoothly in the next version of Sheepify.

### Accomplishments we're proud of

We’re really proud of how much we pulled off in such a short time. On the backend, we built a working login system and leaderboard, introducing a social element that turned Sheepify into a multiplayer experience – way beyond what we first imagined. On the frontend, we’re proud of how much personality we brought to the UI. Almost every inch of the app is filled with hand-drawn sheep, clouds, and dreamy little details, all made using free online paintbrush tools. It feels playful, cohesive, and uniquely ours. Most of all, we’re proud of our persistence – pushing through connection issues, endless debugging, and, ironically, a serious lack of sleep. Even the rain and looming deadlines couldn’t stop us from building something we truly love.

### What we learned

We learned how to build a mobile app from scratch, adapting our code to meet both iOS and Android requirements – a challenge that pushed us way out of our comfort zone. Along the way, we figured out how to merge the frontend and backend effectively, and how to decide which one to prioritize when deadlines were tight. We also discovered how to blend art and engineering – incorporating custom design and hand-drawn visuals directly into the codebase to make the app not just functional, but genuinely fun and visually appealing. Most importantly, we learned to embrace the process of learning and failing, and to value the skills, creativity, and connections we built more than chasing perfection. Sheepify taught us that great projects grow where curiosity and collaboration meet.

### What's next

We have big dreams for Sheepify (pun intended). The next step is bringing our world even more to life – starting with smoother animations, unique sheep skins, and new visual effects to make every user’s farm feel personal. We also plan to build an in-app marketplace, where players can trade or purchase upgrades using their hard-earned wool. Beyond that, we want to expand the social side of Sheepify – adding more multiplayer interactions, friendly competitions, and maybe even midnight prank calls you can buy with in-game currency to “encourage” your friends to go to sleep. At its core, we want Sheepify to grow into a cozy little ecosystem – part wellness tool, part game, part chaotic sleep club – where rest and connection go hand in hand.

## README (from the GitHub repository)

# Welcome to your Expo app 👋

This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).

## Get started

1. Install dependencies

   ```bash
   npm install
   ```

2. Start the app

   ```bash
   npx expo start
   ```

In the output, you'll find options to open the app in a

- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo

You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).

## Get a fresh project

When you're ready, run:

```bash
npm run reset-project
```

This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.

## Learn more

To learn more about developing your project with Expo, look at the following resources:

- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.

## Join the community

Join our community of developers creating universal apps.

- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.


## Detected evidence (automated analysis)

Indexed codebase: 42 recognized source files, 141 KB.
- Anthropic (technology) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- FastAPI (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers

## Codebase structure (from repository index)

### Files (52 of 52)

```
.gitignore
.vscode/extensions.json
.vscode/settings.json
analyze_grid_manual.py
analyze_spawnblocks.py
app.json
app/_layout.tsx
app/(tabs)/_layout.tsx
app/(tabs)/explore.tsx
app/(tabs)/index.tsx
app/farm.tsx
app/modal.tsx
app/shleepy-test.tsx
app/sleep-log.tsx
claude.md
components/external-link.tsx
components/haptic-tab.tsx
components/hello-wave.tsx
components/parallax-scroll-view.tsx
components/SheepSprite.tsx
components/themed-text.tsx
components/themed-view.tsx
components/ui/collapsible.tsx
components/ui/icon-symbol.ios.tsx
components/ui/icon-symbol.tsx
constants/gameRules.ts
constants/theme.ts
convert_tiff_to_png.py
eslint.config.js
expo-env.d.ts
grass.md
hooks/use-color-scheme.ts
hooks/use-color-scheme.web.ts
hooks/use-theme-color.ts
package.json
prompt.md
README.md
scripts/reset-project.js
scripts/resize-sheep.js
services/ai/claudeService.ts
sheep_spawn_grid.json
SHLEEPY_INTEGRATION_PLAN.md
store/gameStore.ts
tsconfig.json
types/game.ts
types/shleepy.ts
uiplan.md
utils/grass_contour_diamond_backup.json
utils/grass_contour.json
utils/sheep_grid_positions.json
utils/sheepSpawner.ts
visualize_contours.py
```

### Dependencies

- package.json: @anthropic-ai/sdk@^0.32.1, @expo-google-fonts/press-start-2p@^0.4.1, @expo/vector-icons@^15.0.3, @react-native-async-storage/async-storage@2.2.0, @react-native-community/datetimepicker@8.4.4, @react-navigation/bottom-tabs@^7.4.0, @react-navigation/elements@^2.6.3, @react-navigation/native@^7.1.8, @types/react@~19.1.0, eslint@^9.25.0, eslint-config-expo@~10.0.0, expo@~54.0.20, expo-constants@~18.0.10, expo-font@~14.0.9, expo-haptics@~15.0.7, expo-image@~3.0.10, expo-linear-gradient@^15.0.7, expo-linking@~8.0.8, expo-router@~6.0.13, expo-splash-screen@~31.0.10, expo-status-bar@~3.0.8, expo-symbols@~1.0.7, expo-system-ui@~6.0.8, expo-web-browser@~15.0.8, react@19.1.0, react-dom@19.1.0, react-native@0.81.5, react-native-gesture-handler@~2.28.0, react-native-reanimated@~4.1.1, react-native-safe-area-context@~5.6.0, react-native-screens@~4.16.0, react-native-web@~0.21.0, react-native-worklets@0.5.1, typescript@~5.9.2, zustand@^5.0.8

### Recent commits (newest first)

- fix: disable all sprite animations for visibility
- style: adjust stats card size and positioning
- feat: add table.png background to stats card
- feat: update log sleep button with text box.png background
- feat: auto-clear sheep on game reload
- style: make test buttons circular with abbreviated labels
- refactor: move test buttons to top right corner
- feat: update Screen 2 background to match Screen 1 gradient
- merge: integrate fix/screen-2 into feature/production-ui with horizontal scrolling and updated Screen 2
- fix: adjust stats card position
- fix: implement dream messages after every wakeup with funny stories
- refactor: change from vertical to horizontal scrolling
- feat: increase speech bubble max lines from 5 to 7
- fix: implement complete page separation with container isolation
- refactor: change page scrolling to separate entities instead of FlatList
- fix: prevent page overflow and keep text box close to top of page 2
- fix: remove delay on rapid clicking for new messages
- feat: show '...' loading indicator while generating new message
- feat: keep chat bubble visible until replaced by new message
- fix: ensure LLM completes thoughts within 100 char limit instead of cutting off

## Key source files (fetched from GitHub, selected and truncated for size)

### prompt.md

```markdown
lets merge screen-2 into production ui branch. Note the following
1. We are going to have horizontal scrolling from now on
2. Screen 2 didn't change anything about screen 1 so nothing on screen 1 should be changed
3. Screen 2 branch implementation of screen2 should replace every aspect of the current production ui's screen 2

if i ask to revert because something broke come back to this point in time
```

### claude.md

```markdown
<role>
You are a senior full-stack developer building Sheepify, a sleep-tracking gamification app. You write production-quality code with proper error handling, type safety, and clear architecture.
</role>

<tech_stack>
Frontend: React Native + TypeScript + Expo
Backend: FastAPI + Uvicorn
Future: LLM integration via API
</tech_stack>

<app_concept>
Sheepify rewards users for sleeping 8-10 hours per night. Each successful night adds a sheep to their farm. Each sheep generates X wool blocks daily. Users spend wool to unlock in-app features.
</app_concept>

<development_workflow>
<branching>
- ALWAYS work on new features in a separate branch
- Branch naming: feature/feature-name or fix/bug-name
- NEVER push directly to main unless explicitly told to
- Only merge to main when user explicitly approves
</branching>

<commits>
- Commit frequently to save progress (every logical unit of work)
- Commit messages format: "type: brief description"
  - Types: feat, fix, refactor, style, test, docs, chore
  - Example: "feat: add sheep generation on successful sleep"
- Commit even incomplete work with WIP prefix if needed
</commits>

<code_quality>
- Full type safety - no 'any' types without justification
- Proper error handling with try/catch and user-friendly messages
- Input validation on both frontend and backend
- Follow React Native/FastAPI best practices
- Component/function names should be self-documenting
</code_quality>
</development_workflow>

<coding_standards>
<typescript>
- Use interfaces for object shapes, types for unions/primitives
- Properly type all function parameters and returns
- Use enums for fixed sets of values
- Enable strict mode
</typescript>

<react_native>
- Functional components with hooks only
- Custom hooks for shared logic
- Proper useEffect cleanup
- Memoization (useMemo/useCallback) only when measured performance gain
</react_native>

<fastapi>
- Pydantic models for request/response validation
- Proper HTTP status codes
- Async endpoints where appropriate
- Structured error responses
</fastapi>
</coding_standards>

<file_organization>
When creating files, organize as:
```
/frontend
  /src
    /components
    /screens
    /hooks
    /services (API calls)
    /types
    /utils
/backend
  /app
    /api (routes)
    /models
    /services (business logic)
    /schemas (Pydantic)
```
</file_organization>

<before_coding>
For each feature request:
1. Create feature branch: git checkout -b feature/feature-name
2. Identify files to create/modify
3. Plan component/module structure
4. Then implement
</before_coding>

<no_documentation>
Do not generate:
- README files unless explicitly asked
- Inline documentation beyond critical complex logic
- API documentation (unless explicitly requested)
</no_documentation>

<communication>
- When showing code changes, show the actual complete file content
- Flag breaking changes or architectural decisions
- Ask clarifying questions only when requirements are ambiguous
- After implementing, state: f
[truncated — 610 more characters]
```

### package.json

```
{
  "name": "sheepify",
  "main": "expo-router/entry",
  "version": "1.0.0",
  "scripts": {
    "start": "expo start",
    "reset-project": "node ./scripts/reset-project.js",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "lint": "expo lint"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.32.1",
    "@expo-google-fonts/press-start-2p": "^0.4.1",
    "@expo/vector-icons": "^15.0.3",
    "@react-native-async-storage/async-storage": "2.2.0",
    "@react-native-community/datetimepicker": "8.4.4",
    "@react-navigation/bottom-tabs": "^7.4.0",
    "@react-navigation/elements": "^2.6.3",
    "@react-navigation/native": "^7.1.8",
    "expo": "~54.0.20",
    "expo-constants": "~18.0.10",
    "expo-font": "~14.0.9",
    "expo-haptics": "~15.0.7",
    "expo-image": "~3.0.10",
    "expo-linear-gradient": "^15.0.7",
    "expo-linking": "~8.0.8",
    "expo-router": "~6.0.13",
    "expo-splash-screen": "~31.0.10",
    "expo-status-bar": "~3.0.8",
    "expo-symbols": "~1.0.7",
    "expo-system-ui": "~6.0.8",
    "expo-web-browser": "~15.0.8",
    "react": "19.1.0",
    "react-dom": "19.1.0",
    "react-native": "0.81.5",
    "react-native-gesture-handler": "~2.28.0",
    "react-native-reanimated": "~4.1.1",
    "react-native-safe-area-context": "~5.6.0",
    "react-native-screens": "~4.16.0",
    "react-native-web": "~0.21.0",
    "react-native-worklets": "0.5.1",
    "zustand": "^5.0.8"
  },
  "devDependencies": {
    "@types/react": "~19.1.0",
    "eslint": "^9.25.0",
    "eslint-config-expo": "~10.0.0",
    "typescript": "~5.9.2"
  },
  "private": true
}

```

### app/_layout.tsx

```typescript
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import 'react-native-reanimated';

import { useColorScheme } from '@/hooks/use-color-scheme';

export const unstable_settings = {
  anchor: '(tabs)',
};

export default function RootLayout() {
  const colorScheme = useColorScheme();

  return (
    <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
      <Stack>
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
      </Stack>
      <StatusBar style="auto" />
    </ThemeProvider>
  );
}

```

### app/(tabs)/_layout.tsx

```typescript
import { Tabs } from 'expo-router';
import React from 'react';

import { HapticTab } from '@/components/haptic-tab';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';

export default function TabLayout() {
  const colorScheme = useColorScheme();

  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
        headerShown: false,
        tabBarButton: HapticTab,
        tabBarStyle: { display: 'none' },
      }}>
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />,
        }}
      />
      <Tabs.Screen
        name="explore"
        options={{
          title: 'Explore',
          tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />,
        }}
      />
    </Tabs>
  );
}

```

### app/(tabs)/index.tsx

```typescript
import { useRouter } from 'expo-router';
import React, { useEffect, useRef, useState } from 'react';
import {
  Dimensions,
  Image,
  ImageBackground,
  ScrollView,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useFonts, PressStart2P_400Regular } from '@expo-google-fonts/press-start-2p';
import { useGameStore } from '../../store/gameStore';
import {
  getRandomPositionInDiamond,
  hasAvailableSpots,
  getOccupiedCount,
  getTotalSpots
} from '../../utils/sheepSpawner';
import { claudeService } from '../../services/ai/claudeService';
import { ShleepyContext } from '../../types/shleepy';

const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');

export default function HomeScreen() {
  const router = useRouter();
  const { user, initializeUser, addSheep, deleteAllSheep, sleepHistory } = useGameStore();
  const sheepPositionsRef = useRef<Map<string, { x: number; y: number }>>(new Map());
  const [, forceUpdate] = useState(0); // Force re-render when positions are loaded
  const [isNightMode, setIsNightMode] = useState(false);
  const [manualOverride, setManualOverride] = useState(false);

  // Shleepy message state
  const [shleepyMessage, setShleepyMessage] = useState<string | null>(null);
  const [isLoadingMessage, setIsLoadingMessage] = useState(false);
  const [lastSleepSessionShown, setLastSleepSessionShown] = useState<string | null>(null);

  // Load retro pixel font
  const [fontsLoaded] = useFonts({
    PressStart2P_400Regular,
  });

  // Check if current time is night (7pm-5am)
  const checkIsNightTime = () => {
    const now = new Date();
    const hour = now.getHours();
    return hour >= 19 || hour < 5; // 7pm (19:00) to 5am
  };

  // Update night mode based on actual time
  useEffect(() => {
    if (!manualOverride) {
      setIsNightMode(checkIsNightTime());

      // Update every minute to check for day/night changes
      const interval = setInterval(() => {
        setIsNightMode(checkIsNightTime());
      }, 60000); // Check every minute

      return () => clearInterval(interval);
    }
  }, [manualOverride]);


  // Toggle day/night for testing
  const toggleDayNight = () => {
    setManualOverride(true);
    setIsNightMode(!isNightMode);
  };

  const handleSpawnSheep = async () => {
    if (!user) return;

    // Check if there are available grid spots
    if (!hasAvailableSpots()) {
      console.log(`[HomeScreen] Grid is full! ${getOccupiedCount()}/${getTotalSpots()} spots occupied`);
      return;
    }

    const platformWidth = SCREEN_WIDTH * 0.84;
    const platformHeight = SCREEN_HEIGHT * 0.48;

    // Get random position from grid system
    const position = await getRandomPositionInDiamond(platformWidth, platformHeight);

    if (!position) {
      console.log('[HomeScreen] No position available - grid is full');
      return;
    }

    // Add new sheep with grid spot ID
    addSheep({
      name: `Sheep #${user.totalSheepEarned + 1}`,
      earnedDate: new Date(),
      woolProduction: 1,
      isAlive: true,
      gridSpotId: position.gridSpotId,
    });
  };

  const handleDeleteAllSheep = () => {
    if (!user) return;
    // Clear stored positions and delete all sheep
    sheepPositionsRef.current.clear();
    deleteAllSheep();
  };

  useEffect(() => {
    // Initialize user if not exists
    if (!user) {
      initializeUser('Shepherd');
    }
    // Clear all sheep on reload to start fresh
    deleteAllSheep();
    sheepPositionsRef.current.clear();
  }, []);


  // Load positions for sheep based on their grid spots
  useEffect(() => {
    const loadSheepPositions = async () => {
      if (!user) return;

      const aliveSheep = user.sheep.filter(s => s.isAlive);
      const platformWidth = SCREEN_WIDTH * 0.84;
      const platformHeight = SCREEN_HEIGHT * 0.48;

      // Load grid positions from JSON
      const gridData = require('../../utils/sheep_grid_positions.json');
      const imageWidth = gridData.imageDimensions.width;
      const imageHeight = gridData.imageDimensions.height;

      for (const sheep of aliveSheep) {
        if (!sheepPositionsRef.current.has(sheep.id)) {
          // If sheep has a grid spot, use that position
          if (sheep.gridSpotId !== undefined) {
            const spot = gridData.spots.find((s: any) => s.id === sheep.gridSpotId);
            if (spot) {
              // Convert from image coordinates to platform coordinates
              const x = (spot.x / imageWidth) * platformWidth - 30; // Center sheep (60/2)
              const y = (spot.y / imageHeight) * platformHeight - 30 - 10; // Move up 10 pixels total
              sheepPositionsRef.current.set(sheep.id, { x, y });
            }
          } else {
            // Fallback for sheep without grid spots (legacy)
            const position = await getRandomPositionInDiamond(platformWidth, platformHeight);
            if (position) {
              sheepPositionsRef.current.set(sheep.id, position);
            }
          }
        }
      }

      // Force re-render to show newly positioned sheep
      forceUpdate(prev => prev + 1);
    };

    loadSheepPositions();
  }, [user?.sheep.length]); // Re-run when sheep count changes

  const getStreakMessage = () => {
    if (!user) return 'Welcome to Sheepify!';

    if (user.streak >= 7) {
      return `Amazing! ${user.streak} night streak!`;
    } else if (user.streak >= 3) {
      return `Great job! ${user.streak} nights in a row!`;
    } else if (user.streak > 0) {
      return `Keep it up! ${user.streak} night streak!`;
    } else if (user.penalties.isInPenalty) {
      return 'Get back on track! 2 good nights needed';
    } else {
      return 'Start your sleep journey tonight!';
    }
  };

  // Check if there's a new sleep session we haven't shown a dream for
  const hasNewSleepSession = () => {
    if (!sleepHistory || sleepHistory.length === 0) return false;
    const latestSleep = sle
[truncated — 14992 more characters]
```

### expo-env.d.ts

```typescript
/// <reference types="expo/types" />

// NOTE: This file should not be edited and should be in your git ignore
```

### eslint.config.js

```javascript
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig } = require('eslint/config');
const expoConfig = require('eslint-config-expo/flat');

module.exports = defineConfig([
  expoConfig,
  {
    ignores: ['dist/*'],
  },
]);

```

### convert_tiff_to_png.py

```python
#!/usr/bin/env python3
"""
Convert farm+windmill TIFF files to PNG format for React Native
"""

from PIL import Image
import os

def convert_tiff_to_png(input_file, output_file):
    """Convert a TIFF image to PNG format"""
    try:
        # Open the TIFF image
        img = Image.open(input_file)

        # Convert to RGB if necessary (TIFF might have different modes)
        if img.mode != 'RGB' and img.mode != 'RGBA':
            img = img.convert('RGBA')

        # Save as PNG
        img.save(output_file, 'PNG', optimize=True)

        # Get file sizes for comparison
        input_size = os.path.getsize(input_file) / (1024 * 1024)  # MB
        output_size = os.path.getsize(output_file) / (1024 * 1024)  # MB

        print(f"✓ Converted {input_file} -> {output_file}")
        print(f"  Input size: {input_size:.2f} MB")
        print(f"  Output size: {output_size:.2f} MB")
        print(f"  Dimensions: {img.size}")

    except Exception as e:
        print(f"✗ Error converting {input_file}: {e}")
        return False

    return True

# Convert both windmill frames
files_to_convert = [
    ('farm+windmill-1.tiff', 'farm-windmill-1.png'),
    ('farm+windmill-2.tiff', 'farm-windmill-2.png')
]

for input_file, output_file in files_to_convert:
    if os.path.exists(input_file):
        convert_tiff_to_png(input_file, output_file)
    else:
        print(f"✗ File not found: {input_file}")
```

### visualize_contours.py

```python
#!/usr/bin/env python3
"""
Visualize the green contours that are used for sheep spawning
"""

import cv2 as cv
import numpy as np
import json

# Load the current grass contour from the JSON file
try:
    with open('utils/grass_contour.json', 'r') as f:
        contour_points = json.load(f)
    contour = np.array(contour_points, dtype=np.int32)
    print(f"Loaded grass_contour.json with {len(contour_points)} points")
except Exception as e:
    print(f"Error loading contour: {e}")
    contour = None

# Load both windmill frames to show contours on them
images_to_process = [
    ('farm-windmill-1.png', 'contour_vis_windmill1.png'),
    ('farm-windmill-2.png', 'contour_vis_windmill2.png'),
    ('blockofdirt.png', 'contour_vis_blockofdirt.png')
]

for input_file, output_file in images_to_process:
    try:
        # Load the image
        img = cv.imread(input_file)
        if img is None:
            print(f"Skipping {input_file} - not found")
            continue

        # Create a visualization copy
        vis = img.copy()

        # Method 1: Draw the loaded contour from JSON (in red)
        if contour is not None:
            cv.drawContours(vis, [contour], -1, (0, 0, 255), 3)  # Red thick line

        # Method 2: Also detect green areas fresh to compare (in blue)
        hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV)
        hsv_green_low = (35, 40, 40)
        hsv_green_high = (90, 255, 255)
        green_mask = cv.inRange(hsv, np.array(hsv_green_low), np.array(hsv_green_high))

        # Find contours in current image
        current_contours, _ = cv.findContours(green_mask, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
        if current_contours:
            largest = max(current_contours, key=cv.contourArea)
            cv.drawContours(vis, [largest], -1, (255, 0, 0), 2)  # Blue thin line

        # Add a semi-transparent green overlay on the detected area
        overlay = vis.copy()
        mask_colored = cv.cvtColor(green_mask, cv.COLOR_GRAY2BGR)
        mask_colored[:, :, 0] = 0  # Remove blue channel
        mask_colored[:, :, 2] = 0  # Remove red channel
        overlay = cv.addWeighted(vis, 0.7, mask_colored, 0.3, 0)

        # Add legend
        cv.putText(overlay, "Red = Stored Contour (grass_contour.json)", (50, 50),
                   cv.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
        cv.putText(overlay, "Blue = Fresh Detection", (50, 90),
                   cv.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2)
        cv.putText(overlay, "Green Tint = HSV Green Areas", (50, 130),
                   cv.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)

        # Save the visualization
        cv.imwrite(output_file, overlay)
        print(f"✓ Created {output_file}")

        # Also save just the mask for clarity
        mask_output = output_file.replace('.png', '_mask.png')
        cv.imwrite(mask_output, green_mask)
        print(f"  Saved mask to {mask_output}")

    except Exception as e:
        print(f"✗ Error processing {input_file}: {e}")

print("\nContour visualizations created!")
print("Red lines show the stored contour from grass_contour.json")
print("Blue lines show freshly detected green areas")
print("Green tint shows all pixels detected as green by HSV threshold")
```

[32 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]