# Project export: Bee Well

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: TreeHacks 2025
- Tagline: Gamified neck pain relief
- Devpost: https://devpost.com/software/bee-well-03t1a9
- GitHub: https://github.com/lindaktong/treehacks
- Demo: https://docs.google.com/presentation/d/1LWnt_5TzodOZ0M6csjatNXX-jH3G9vShAxK544DNvdI/edit?usp=sharing
- Video: https://www.youtube.com/embed/GjMDGP7s34I?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Linda Tong (4 commits), Nancy (3 commits), Angela Yuan (1 commits)

## Devpost submission (written by the team)

### Overview

Introducing Bee Well A neck exercise game to help you move your neck! In the game you play as a busy-bee going from flower to flower. As programmers, we spend countless hours at our computers—and we know how neck and back pain can cramp your style. Bee Well is our gamified solution to this common problem. At TreeHacks, we tested our product with over 20 hackers. We’ve transformed neck exercises into an immersive gaming experience that not only alleviates pain but also makes moving fun! What It Does Bee Well challenges you to guide a bee to a flower—all using head tilts! Motion-Driven Gameplay: Using Apple’s CMHeadphoneMotionManager, we track your head movements in real time. Tilt your head (yaw, roll, and pitch) to control the bee’s path. Motion-Driven Gameplay: Using Apple’s CMHeadphoneMotionManager, we track your head movements in real time. Tilt your head (yaw, roll, and pitch) to control the bee’s path. Spatial Audio Cues: Our dynamic audio engine adjusts volume and panning based on distance and direction, letting you play with your eyes closed if you dare. Spatial Audio Cues: Our dynamic audio engine adjusts volume and panning based on distance and direction, letting you play with your eyes closed if you dare. Adaptive Challenges: With a built-in countdown timer and collision detection, each round provides a fun, engaging challenge that encourages regular neck movement and exercise. Adaptive Challenges: With a built-in countdown timer and collision detection, each round provides a fun, engaging challenge that encourages regular neck movement and exercise. Check out our demo video to see Bee Well in action! How We Built It Design Process Tech Stack: SwiftUI & AVFoundation: For a sleek and responsive user interface and immersive sound integration. SwiftUI & AVFoundation: For a sleek and responsive user interface and immersive sound integration. Apple Core Motion: Capturing head tilt data (yaw, roll, and pitch) in real time from your AirPods. Apple Core Motion: Capturing head tilt data (yaw, roll, and pitch) in real time from your AirPods. Dynamic Audio Processing: Leveraging spatial audio to create realistic directional cues that adapt as you move. Dynamic Audio Processing: Leveraging spatial audio to create realistic directional cues that adapt as you move. Our Approach: Real-Time Head Tracking: We fine-tuned head tracking to be both responsive and smooth, ensuring that every tilt translates into precise bee navigation. Real-Time Head Tracking: We fine-tuned head tracking to be both responsive and smooth, ensuring that every tilt translates into precise bee navigation. Spatial Audio Magic: By dynamically adjusting audio cues, we help you intuitively locate your target—a virtual flower—making the experience both engaging and therapeutic. Spatial Audio Magic: By dynamically adjusting audio cues, we help you intuitively locate your target—a virtual flower—making the experience both engaging and therapeutic. Iterative Development: We worked together to overcome technical challenges, iterating rapidly and collaborating closely to ensure a fluid, enjoyable experience. Iterative Development: We worked together to overcome technical challenges, iterating rapidly and collaborating closely to ensure a fluid, enjoyable experience. Challenges We Ran Into Balancing Responsiveness & Smoothness: Fine-tuning head tracking and designing an audio-interactive experience that is intuitive and delightful. Balancing Responsiveness & Smoothness: Fine-tuning head tracking and designing an audio-interactive experience that is intuitive and delightful. Crafting Natural Audio Feedback: Designing spatial audio that feels intuitive and immersive. Crafting Natural Audio Feedback: Designing spatial audio that feels intuitive and immersive. Seamless UI Performance: Ensuring our UI remains fluid while processing continuous head movement data. Seamless UI Performance: Ensuring our UI remains fluid while processing continuous head movement data. Building a Narrative: Creating an engaging story around a bee that keeps players coming back for more. Building a Narrative: Creating an engaging story around a bee that keeps players coming back for more. Accomplishments We're Proud Of Controller-Free Navigation: Implemented head-tilt-based navigation—no external controllers needed. Audio allows users with low vision to also be able to play. Controller-Free Navigation: Implemented head-tilt-based navigation—no external controllers needed. Audio allows users with low vision to also be able to play. Immersive Adaptive Audio: Developed real-time adaptive audio feedback that makes the game playable even with your eyes closed. Immersive Adaptive Audio: Developed real-time adaptive audio feedback that makes the game playable even with your eyes closed. Accessible & Engaging Experience: Designed a game that not only provides a fun distraction but also encourages healthier movement habits – we built something we ourselves will use. Accessible & Engaging Experience: Designed a game that not only provides a fun distraction but also encourages healthier movement habits – we built something we ourselves will use. What We Learned Using Apple’s Core Motion framework to track head movements with precision. How to create a fun interactive game that uses the body as a controller Learning what makes people feel satisfied in a game loop Accessing information from airpods and repurposing data to be used as a game mechanic Gathering user feedback from playtesting Building an intuitive user experience that is also easy for users to grasp What's Next for Bee Well These are the features we would like to work on next: Daily Challenges: Dynamic, daily challenges that allow people to come back and continue to improve on their neck mobility and strength. Daily Challenges: Dynamic, daily challenges that allow people to come back and continue to improve on their neck mobility and strength. Expanded Gameplay: Add levels with obstacles, different storylines, environmental changes, and more audio-led experiences. When we were thinking about what games can take advantage of motion control, many options came to mind, from Pong to 2048. We would like to diversify the game options users have. Expanded Gameplay: Add levels with obstacles, different storylines, environmental changes, and more audio-led experiences. When we were thinking about what games can take advantage of motion control, many options came to mind, from Pong to 2048. We would like to diversify the game options users have. Multiplayer Modes: Enable collaborative challenges and competitive play for enhanced social engagement. Multiplayer Modes: Enable collaborative challenges and competitive play for enhanced social engagement. VR/AR Integration: Explore mixed-reality experiences to further elevate immersion and interactivity, like using the gyroscope from a headset to create a digital landscape shaped by head motion. VR/AR Integration: Explore mixed-reality experiences to further elevate immersion and interactivity, like using the gyroscope from a headset to create a digital landscape shaped by head motion. AI-Powered Analytics: Integrating AI to provide personalized feedback on neck movement trends and overall well-being. AI-Powered Analytics: Integrating AI to provide personalized feedback on neck movement trends and overall well-being.

## README (from the GitHub repository)

# Gamified neck pain relief #


https://github.com/user-attachments/assets/c7108aa1-2087-47b1-9ddd-675558f54ea1

As programmers, we spend countless hours at our computers—and we know how neck and back pain can cramp your style. **Bee Well** is our gamified solution to this common problem. We’ve transformed neck exercises into an immersive gaming experience that not only alleviates pain but also makes moving fun!


## Detected evidence (automated analysis)

Indexed codebase: 10 recognized source files, 21 KB.
- Swift (language) — detected in the code

## Codebase structure (from repository index)

### Files (31 of 31)

```
.DS_Store
README.md
treehacks.xcodeproj/project.pbxproj
treehacks.xcodeproj/project.xcworkspace/contents.xcworkspacedata
treehacks.xcodeproj/project.xcworkspace/xcuserdata/nancy.xcuserdatad/UserInterfaceState.xcuserstate
treehacks.xcodeproj/xcuserdata/linda.xcuserdatad/xcschemes/xcschememanagement.plist
treehacks.xcodeproj/xcuserdata/nancy.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
treehacks.xcodeproj/xcuserdata/nancy.xcuserdatad/xcschemes/xcschememanagement.plist
treehacks/.DS_Store
treehacks/Assets.xcassets/.DS_Store
treehacks/Assets.xcassets/AccentColor.colorset/Contents.json
treehacks/Assets.xcassets/again.imageset/Contents.json
treehacks/Assets.xcassets/AppIcon.appiconset/Contents.json
treehacks/Assets.xcassets/bug.imageset/Contents.json
treehacks/Assets.xcassets/bug2.imageset/Contents.json
treehacks/Assets.xcassets/bzzz.imageset/Contents.json
treehacks/Assets.xcassets/Contents.json
treehacks/Assets.xcassets/enuf.imageset/Contents.json
treehacks/Assets.xcassets/flower_black.imageset/Contents.json
treehacks/Assets.xcassets/flower_purple.imageset/Contents.json
treehacks/Assets.xcassets/square.imageset/Contents.json
treehacks/AudioManager.swift
treehacks/BeeTrailView.swift
treehacks/ContentView.swift
treehacks/HeadTiltDelegate.swift
treehacks/Preview Content/Preview Assets.xcassets/Contents.json
treehacks/RestartView.swift
treehacks/treehacksApp.swift
treehacksTests/treehacksTests.swift
treehacksUITests/treehacksUITests.swift
treehacksUITests/treehacksUITestsLaunchTests.swift
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Create README.md
- app icons, splashscreen, end screen
- bzzzz sound
- most updated version
- added sound
- my neck hurts
- airpods files
- Initial Commit

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

### treehacksTests/treehacksTests.swift

```swift
//
//  treehacksTests.swift
//  treehacksTests
//
//  Created by Linda on 2/15/25.
//

import Testing
@testable import treehacks

struct treehacksTests {

    @Test func example() async throws {
        // Write your test here and use APIs like `#expect(...)` to check expected conditions.
    }

}

```

### treehacks/treehacksApp.swift

```swift
//
//  treehacksApp.swift
//  treehacks
//
//  Created by Linda on 2/15/25.
//

import SwiftUI

//@main
//struct treehacksApp: App {
//    var body: some Scene {
//        WindowGroup {
//            ContentView()
//        }
//    }
//}

@main
struct treehacksApp: App {
    var body: some Scene {
        WindowGroup {
            SplashScreenView()
        }
    }
}

```

### treehacksUITests/treehacksUITestsLaunchTests.swift

```swift
//
//  treehacksUITestsLaunchTests.swift
//  treehacksUITests
//
//  Created by Linda on 2/15/25.
//

import XCTest

final class treehacksUITestsLaunchTests: XCTestCase {

    override class var runsForEachTargetApplicationUIConfiguration: Bool {
        true
    }

    override func setUpWithError() throws {
        continueAfterFailure = false
    }

    @MainActor
    func testLaunch() throws {
        let app = XCUIApplication()
        app.launch()

        // Insert steps here to perform after app launch but before taking a screenshot,
        // such as logging into a test account or navigating somewhere in the app

        let attachment = XCTAttachment(screenshot: app.screenshot())
        attachment.name = "Launch Screen"
        attachment.lifetime = .keepAlways
        add(attachment)
    }
}

```

### treehacks/RestartView.swift

```swift
import SwiftUI

struct RestartView: View {
    var restartAction: () -> Void

    var body: some View {
        ZStack {
            // Full-screen background image
            Image("enuf")
                .resizable()
                .scaledToFill()
                .ignoresSafeArea()
            
            // "Play Again" button positioned at the bottom
            VStack {
                Spacer()
                Button(action: restartAction) {
                    Image("again")
                        .resizable()
                        .scaledToFit()
                        .frame(maxWidth: 200)  // Adjust size as needed
                }
                .padding(.bottom, 150)  // Adjust bottom spacing as needed
            }
        }
    }
}

struct RestartView_Previews: PreviewProvider {
    static var previews: some View {
        RestartView(restartAction: {})
    }
}

```

### treehacks/BeeTrailView.swift

```swift
// BeeTrailView.swift
import SwiftUI

struct BeeTrailView: View {
    var points: [CGPoint]
    
    var body: some View {
        Path { path in
            guard let first = points.first else { return }
            path.move(to: first)
            for point in points.dropFirst() {
                path.addLine(to: point)
            }
        }
        .stroke(
            style: StrokeStyle(
                lineWidth: 2,
                lineCap: .round,
                lineJoin: .round,
                dash: [5, 20]  // 5 points drawn, 10 points gap
            )
        )
        .foregroundColor(.gray)
        .opacity(0.3)
    }
}

struct BeeTrailView_Previews: PreviewProvider {
    static var previews: some View {
        BeeTrailView(points: [
            CGPoint(x: 50, y: 50),
            CGPoint(x: 100, y: 100),
            CGPoint(x: 150, y: 80)
        ])
        .frame(width: 300, height: 300)
    }
}

```

### treehacksUITests/treehacksUITests.swift

```swift
//
//  treehacksUITests.swift
//  treehacksUITests
//
//  Created by Linda on 2/15/25.
//

import XCTest

final class treehacksUITests: XCTestCase {

    override func setUpWithError() throws {
        // Put setup code here. This method is called before the invocation of each test method in the class.

        // In UI tests it is usually best to stop immediately when a failure occurs.
        continueAfterFailure = false

        // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
    }

    override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }

    @MainActor
    func testExample() throws {
        // UI tests must launch the application that they test.
        let app = XCUIApplication()
        app.launch()

        // Use XCTAssert and related functions to verify your tests produce the correct results.
    }

    @MainActor
    func testLaunchPerformance() throws {
        if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
            // This measures how long it takes to launch your application.
            measure(metrics: [XCTApplicationLaunchMetric()]) {
                XCUIApplication().launch()
            }
        }
    }
}

```

### treehacks/AudioManager.swift

```swift
//
//  AudioManager.swift
//  treehacks
//
//  Created by Linda on 2/16/25.
//

import AVFoundation

class AudioManager {
    static let shared = AudioManager()
    private var audioPlayers: [AVAudioPlayer] = []

    private init() {}

    func playBackgroundMusic(_ fileName: String, fileType: String = "mp3", volume: Float = 0.5, pan: Float = 0.0) {
        guard let url = Bundle.main.url(forResource: fileName, withExtension: fileType) else {
            print("Music file not found")
            return
        }

        do {
            let audioPlayer = try AVAudioPlayer(contentsOf: url)
            audioPlayer.volume = volume
            audioPlayer.pan = pan
            audioPlayer.numberOfLoops = -1
            audioPlayer.prepareToPlay()
            audioPlayer.play()

            audioPlayers.append(audioPlayer)
        } catch {
            print("Error playing music: \(error.localizedDescription)")
        }
    }

    func updateBackgroundMusicVolume(_ volume: Float, pan: Float) {
        DispatchQueue.main.async {
            self.audioPlayers.forEach {
                $0.volume = volume
                $0.pan = pan // Apply left-right audio panning
            }
        }
    }

    func playSound(_ fileName: String, fileType: String = "mp3", volume: Float = 1.0) {
        guard let url = Bundle.main.url(forResource: fileName, withExtension: fileType) else {
            print("Sound file not found")
            return
        }

        do {
            let audioPlayer = try AVAudioPlayer(contentsOf: url)
            audioPlayer.volume = volume
            audioPlayer.prepareToPlay()
            audioPlayer.play()

            audioPlayers.append(audioPlayer)
        } catch {
            print("Error playing sound: \(error.localizedDescription)")
        }
    }

    func stopAllMusic() {
        for player in audioPlayers {
            player.stop()
        }
        audioPlayers.removeAll()
    }
}

```

### treehacks/HeadTiltDelegate.swift

```swift
//
//  HeadTiltDelegate.swift
//  treehacks
//
//  Created by Linda on 2/15/25.
//

import Foundation
import CoreMotion
import Combine

protocol HeadTiltDelegate: AnyObject {
    func startTilt()
    func endTilt()
}





class HeadTiltDetector: NSObject, ObservableObject, CMHeadphoneMotionManagerDelegate {
    private let motionManager = CMHeadphoneMotionManager()
    private let tiltThresholdDeg = 28.0
    private let tiltThresholdEndDeg = 20.0
    
    var delegate: HeadTiltDelegate?
    
    @Published var isTracking = false
    @Published var headphonesConnected = false
    @Published var isTilting = false
    @Published var tiltDeg = 0.0
    
    // New published properties for head orientation
    @Published var yaw: Double = 0.0
    @Published var pitch: Double = 0.0
    @Published var roll: Double = 0.0
    
    func startDetectingHeadTilt() {
        motionManager.delegate = self
        // Check if the device supports headphone motion updates
        guard motionManager.isDeviceMotionAvailable else {
            print("Headphone motion updates are not available")
            return
        }
        
        // Start receiving headphone motion updates
        motionManager.startDeviceMotionUpdates(to: .main) { [weak self] deviceMotion, error in
            if let error = error {
                print("Error receiving headphone motion updates: \(error)")
                return
            }
            guard let self = self, let deviceMotion = deviceMotion else { return }
            
            // Debug print to see if you're getting roll data
//            print("Roll: \(deviceMotion.attitude.roll * 180 / .pi)°")
            
            self.handleDeviceMotion(deviceMotion)
        }
        isTracking = true
    }
    
    // Receive headphone connect updates so we can update the UI
    func headphoneMotionManagerDidConnect(_ manager: CMHeadphoneMotionManager) {
        headphonesConnected = true
    }
    
    func headphoneMotionManagerDidDisconnect(_ manager: CMHeadphoneMotionManager) {
        headphonesConnected = false
    }
    
    
    
    private func handleDeviceMotion(_ deviceMotion: CMDeviceMotion) {
        // Update the published orientation values in degrees
        self.yaw = deviceMotion.attitude.yaw * 180 / .pi
        self.pitch = deviceMotion.attitude.pitch * 180 / .pi
        self.roll = deviceMotion.attitude.roll * 180 / .pi
        
        // Also update tiltDeg for your tilt detection logic
        tiltDeg = self.roll
        
        // Debug print orientation values
//        print("yaw: \(self.yaw) deg, roll: \(self.roll) deg, pitch: \(self.pitch) deg")
        
        // Check tilt thresholds using the updated roll value
        let tiltThresholdMet = self.roll < -tiltThresholdDeg
        let tiltEndThresholdMet = self.roll > -tiltThresholdEndDeg
        
        if tiltThresholdMet && !isTilting {
            isTilting = true
            delegate?.startTilt()
        } else if tiltEndThresholdMet && isTilting {
            isTilting = false
            delegate?.endTilt()
        }
    }
    
    func stopDetectingHeadTilt() {
        motionManager.stopDeviceMotionUpdates()
        isTracking = false
    }
}

```

### treehacks/ContentView.swift

```swift
import SwiftUI
import AVFoundation

/// A small struct that holds yaw and pitch values.
/// Marked `Equatable` so we can use `onChange(of:)`.
struct YawPitch: Equatable {
    let yaw: Double
    let pitch: Double
}

struct SplashScreenView: View {
    @State private var isActive = false
    @State private var opacity = 0.0
  

    var body: some View {
        if isActive {
            ContentView() // Go to main screen after animation
        } else {
            VStack {
                Image("flower_purple") // Your logo
                    .resizable()
                    .scaledToFit()
                    .frame(width: 150, height: 150)
                    .opacity(opacity)
                    .onAppear {
                        withAnimation(.easeIn(duration: 2.0)) {
                            opacity = 1.0
                        }
                    }
                Image("bzzz")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 120, height: 120)
            }
            .onAppear {
                AudioManager.shared.playSound("bee_sound")
                DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
                    withAnimation {
                        isActive = true
                    }
                }
            }
        }
    }
}

struct ContentView: View {
    @StateObject private var headTiltDetector = HeadTiltDetector()
    
    // Square properties
    @State private var squarePosition: CGPoint = .zero
    @State private var squareImage: String = "flower_black" // Initial image
    @State private var collisionDetected = false
    @State private var audioPlayer: AVAudioPlayer?
    
    @State private var gamesPlayed: Int = 0 // Track the number of games played
       
    // Game time properties
    @State private var timeRemaining: Int = 60
    @State private var gameEnded = false
    @State private var fadeInText = false

    @State private var restartTimer: Timer?
    @State private var countdownToRestart = 7 // 7 seconds countdown
    
    // New state variable to trigger the restart screen
    @State private var showRestartScreen = false
    
    // Bee trail state: record positions of the bug
    @State private var beeTrail: [CGPoint] = []
    
    let squareSize: CGFloat = 75
    
    var body: some View {
        GeometryReader { geometry in
            ZStack {
                Color.white.ignoresSafeArea()
                
                // Draw the bee trail behind everything
                BeeTrailView(points: beeTrail)
                
                // Square (target)
                ZStack {
                    Circle()
                        .frame(width: squareSize, height: squareSize)
                    Image(squareImage)
                        .resizable()
                        .scaledToFit()
                        .frame(width: squareSize, height: squareSize)
                        .clipShape(Circle())
                }
                .position(squarePosition)
                // Blue dot whose position depends on yaw/pitch
//                Circle()
//                    .fill(Color.blue)
//                    .frame(width: 30, height: 30)
//                    .position(
//                        x: calculateX(in: geometry.size, yaw: headTiltDetector.yaw),
//                        y: calculateY(in: geometry.size, pitch: headTiltDetector.pitch)
//                    )
                Image("bug2") // Replace with your actual image asset name
                    .resizable()
                    .scaledToFit()
                    .frame(width: 30, height: 30)
                    .clipShape(Circle())
                    .position(
                        x: calculateX(in: geometry.size, yaw: headTiltDetector.yaw),
                        y: calculateY(in: geometry.size, pitch: headTiltDetector.pitch)
                    )
                
                // Debug info at the bottom
                VStack {
                    Spacer()
                    if !gameEnded {
                        Text("\(timeRemaining)")
                            .foregroundColor(timeRemaining <= 10 ? Color(red: 229/255.0, green: 0, blue: 255/255.0) : .black)
                            .padding(.bottom, 20)
                    } else {
                        Text("game over")
                            .font(.title)
                            .foregroundColor(.black)
                            .padding(.bottom, 20)
                    }
                }
            }
            .onAppear {
                AudioManager.shared.playBackgroundMusic("calm_music")
                headTiltDetector.startDetectingHeadTilt()
                
                // Initialize square in center if not set yet
                if squarePosition == .zero {
                    squarePosition = CGPoint(
                        x: geometry.size.width / 2,
                        y: geometry.size.height / 2
                    )
                }
                startGameTimer()
            }
            .onDisappear {
                headTiltDetector.stopDetectingHeadTilt()
            }
            // Update audio settings, check for collisions, and update bee trail
            .onChange(of: YawPitch(yaw: headTiltDetector.yaw, pitch: headTiltDetector.pitch)) { newValue in
                let dotX = calculateX(in: geometry.size, yaw: newValue.yaw)
                let dotY = calculateY(in: geometry.size, pitch: newValue.pitch)
                
                // Append the new position to the bee trail and limit its length if needed
                let newPoint = CGPoint(x: dotX, y: dotY)
                beeTrail.append(newPoint)
                if beeTrail.count > 50 { // Limit to last 50 points
                    beeTrail.removeFirst()
                }
                
                let distance = calculateDistance(dotX: dotX, dotY: dotY,
                                                 squareX: 
[truncated — 5201 more characters]
```