diff --git a/README.md b/README.md index 65d958c..f2a4e1c 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,158 @@ -Oust -==== -Also known as Candle Fight, Jonty Sebastian Joust, and "that game with the lights". +# Oust +Also known as Candle Fight, Jonty Sebastian Joust, and "that game with the lights" A love letter to [Johann Sebastian Joust](http://www.jsjoust.com/), designed for playing outdoors at noisy festivals in the dark. All night long (all night). +## How It Works +The aim of the game is to keep your orb lit by holding it steady. If you move it too fast, the light goes out and you lose. + +Hold yours steady while trying to make other players move their orbs too fast. You could: + +* Make them flinch. +* Hit their controller. +* Wait for them to do something silly. + +The winner is the last person whose orb stays alight + + ![Rpi Oust Setup](sandwich_box.jpg) -A long time ago on a boat far far away --------------------------------------- +## A long time ago on a boat far far away When organising [Electromagnetic Wave](https://www.emfcamp.org/wave) in early 2013 we asked some of the [Wild Rumpus](http://thewildrumpus.co.uk/) organisers to run evening games on the boat. A very beta version of Joust was run on the deck to rave reviews. Fast forward to 2015. We've taken Joust (in both beta and final versions) to [OHM](https://en.wikipedia.org/wiki/Observe._Hack._Make.) in 2013 and [EMF](https://www.emfcamp.org) in 2014. Every time the game runs all night with almost no supervision. By this point the game has developed a bit of a following and people are actively seeking us out to play. But there are problems. Joust requires a laptop to run (meaning we need to be near a power supply and can't leave it alone), it needs speakers for the music (which we never have), and people have trouble understanding the gameplay without announcements and cues. -I went to [CCC](https://events.ccc.de/camp/2015/wiki/Main_Page) in 2015 prepared. We had 11 move controllers, three Raspberry Pi's, 46 Ah of portable batteries, and more bluetooth adapters than there are bluetooth channels. +I went to [CCC](https://events.ccc.de/camp/2015/wiki/Main_Page) in 2015 prepared. We had 11 move controllers, three Raspberry Pis, 46 Ah of portable batteries, and more bluetooth adapters than there are bluetooth channels. -Oust is the result. It runs on a Raspberry Pi 2/3, can run up to 8 controllers (maybe more), and is specifically designed for environments so loud sign language is required. +Oust is the result. It runs on a Raspberry Pi, can run up to 8 controllers (maybe more), and is specifically designed for environments so loud sign language is required. It was playtested and tweaked extensively at CCC - at one point with a circle of approximately 150 people playing & watching. The longest game I recorded finished after 9 hours of continuous play. -How to Oust ------------ + +## How to Oust 1. Turn the Raspberry Pi on. 2. Turn the controllers on. 3. Play! -No really how does one Oust ---------------------------- +## No really how does one Oust The game is designed to run on a Raspberry Pi hooked up to a honking great battery pack. There is no interface other than the controllers themselves. If you were starting out from scratch this is what it would look like: -1. Install Oust on the Raspberry Pi, including the nasty bluetooth hacks (See below). +1. Install Oust on the Raspberry Pi. 2. Plug the Pi into your battery pack. -3. Connect each controller to the Pi in turn using a MiniUSB cable. When the controller goes white it has been paired. +3. Connect each controller to the Pi in turn using a MiniUSB cable. When the orb goes white it has been paired. 4. Turn on as many controllers as you want to play with. 5. Each player presses the trigger, and the game begins. 6. Optionally put the Pi & battery in a plastic sandwich box. -But what does the game look like? ---------------------------------- -1. All the controllers are dark. As the players press the squishy trigger their controller lights up white. -2. When all the controllers are white (or someone presses Triangle) the controllers will vibrate, then flash red/yellow/green as a "get ready" signal. -3. Every controller turns a different colour and the game has begun. -4. The aim of the game is to force all the other players to move their controllers too fast, either by hitting the controller, making them flinch, or the other player doing something stupid. -5. If your controller is going too fast it'll flicker as a warning. -6. If you are knocked out, your controller goes dark and vibrates. -7. The last player standing has their controller flash a beautiful rainbow sequence, and all controllers vibrate to indicate the end of the game. -8. The game resets, people hand their controllers to other people to play. GOTO 1. - -Amazing Features ----------------- + +## What does the game look like? +1. All the controllers' orbs glow dark orange to show that they're connected. +1. Each player pulls the squishy trigger to join the game, and their controller lights up white to show they're ready to play. +1. When all the controllers are white (or when someone presses START) the controllers will vibrate, then flash red/yellow/green as a "get ready" signal. +1. Every controller turns a different colour and the game has begun. +1. The aim of the game is to force all the other players to move their controllers too fast, either by hitting the controller, making them flinch, or the other player doing something stupid. +1. If your controller is going too fast it'll flicker as a warning. +1. If you are knocked out, your controller goes dark and vibrates. +1. The last player standing is the winner! And their controller flashes a beautiful rainbow sequence, and all controllers vibrate to indicate the end of the game. +1. The game resets, people hand their controllers to other people to play. GOTO 1. + +## Amazing Features * Instant setup * Easy pairing -* Add/remove controllers to the game on the fly -* Battery checking button (Press circle) -* Secret goddamn-it-start-the-game button (Press the START button) -* Turn off controller button before game start (Press the SELECT button) +* Add/remove controllers to/from the game on the fly +* Battery checking button (Press ⭕️ CIRCLE) +* Secret "goddamn it start the game" button (Press START) +* Turn off controller before the game starts (Press SELECT or PS) * "Ready, Steady, Go" start sequence to get players attention * Going-too-fast warning * Support for as many controllers as bluetooth interference will allow -How to install Oust from an image ---------------------------------- -[@hdsjulian](https://twitter.com/hdsjulian/status/733990403075104768) has built a Raspbian image with Oust preinstalled and preconfigured, flash it in the same way you'd flash any other Raspberry Pi image: http://phinn.de/oustpi.img.zip (sha256sum: 7db3147ba2c7a2d8910a16c02eb7ed84424a5bdd39686bc30ea61b7820f99a8b) - -How to install Oust Manually ----------------------------- -1. Get a Raspberry Pi 2 -2. Install Raspbian on the SD card -3. Checkout PSMoveAPI to the pi user's home directory (https://github.com/thp/psmoveapi) -4. Merge in this patch to fix compilation on the Rpi https://github.com/thp/psmoveapi/pull/184 -5. `apt-get install swig python-dev bluez` -6. Build PSMoveAPI standalone (5.b here https://github.com/thp/psmoveapi/blob/master/README.ubuntu) -7. Copy [oust.py](oust.py) to /home/pi/psmoveapi/build/ -8. `apt-get install supervisor` -9. Replace /etc/supervisor with the contents of [supervisor](supervisor) -10. Copy [scan_enable.sh](scan_enable.sh) to /home/pi/ -11. Reboot the pi and pair your controllers! +## Setup and Installation +### Essentials +* Raspberry Pi 3B +* MicroSD Card +* 4x PS3 Move Controller (1st generation) [🛍️ CEX](https://uk.webuy.com/product-detail/?id=SP3MCON001) +* 1+ Mini USB cable for pairing the PSMove controllers * 1x Micro USB cable for powering Raspberry Pi +* 1x USB Power Supply >=2.5A (12.5W+) + +**Beware: The PSMove controllers will only charge over USB when connected to a USB Data Port!** They should however charge ok on a PSMove controller dock that uses the two metal contacts instead of USB. + +### Install Software on Raspberry Pi + +1. Install the operating system on an SD Card, using the official [Raspberry Pi Imager](https://www.raspberrypi.com/software/) with these settings: + * **OS**: Raspberry Pi OS Other / Raspberry Pi OS Lite (64-bit) + * **Hostname**: oust + * **Username/Password**: oust/oust + * **Wifi**: connect to local network for setup + * **Remote Access**: SSH enabled + * **Raspberry Pi Connect**: Disabled +1. Boot Pi +1. SSH in `ssh oust@oust` or connect KVM to install stuff: + + ``` + sudo apt update + sudo apt install git vim bluez + ``` +1. Install PSMove API + 1. Download Current PSMove API and install dependencies + + ``` + git clone https://github.com/thp/psmoveapi.git + cd psmoveapi + git submodule update --init + bash -e -x scripts/install_dependencies.sh + ``` + * This took surprisingly long. + * In theory a load of these dependencies can probably be removed, as we're not building all the products. + 1. Build PSMove API. + + ``` + mkdir build + cd build + cmake .. -DPSMOVE_BUILD_TRACKER=OFF + make + ``` + * If the above doesnt build, this is the script that comes with PSMoveAPI, but it builds loads more stuff than Oust uses. `bash -e -x scripts/linux/build-debian` +1. Change Bluetooth input settings to allow it to pair to PSMove controller. + + ``` + echo -e "\n# Setting for PSMove to connect\nClassicBondedOnly=false" | sudo tee -a /etc/bluetooth/input.conf > /dev/null + tail -n16 /etc/bluetooth/input.conf + ``` + +1. Install Oust + + ``` + cd ~ + git clone https://github.com/MattGrayYes/Oust.git + cd Oust + ``` + +1. Check that Oust runs `sudo python oust.py` + * Pair at least one controller at this point to test it all works with bluetooth. + * Press `CTRL + C` to exit + +1. Set up Oust in SystemD, so it auto-runs on boot. + 1. Install the config file `sudo cp oust.service /etc/systemd/system/` + 1. Start the service: `sudo systemctl start oust.service` + 1. Take a sneak peek at the service `systemctl status oust.service` + * If Oust is running, the paired controller should pair and glow orange when turned on. + * You can also monitor the service live as it runs `journalctl --follow --unit=oust.service --lines=20` + * `CTRL + C` to exit. + 1. If it all seems fine, enable the service `sudo systemctl enable oust.service` + 1. Reboot `sudo reboot` + 1. When it reboots, Oust should be running: + 1. If you turn on a paired controller, it should glow orange + 1. If you plug in a new controller, it should pair + 1. To double check, log back in and look at the service status `systemctl status oust.service` + * It should be enabled and active/running + Things You Should Know ---------------------- * The Playstation Move controllers actually implement the USB 1.2 standard, which means they NEED a data connection to charge. Essentially, you must connect them to a computer to charge them, a wall wart won't do. -* Building PSMoveAPI is horrid. I'll publish debian packages soon. -* The Raspberry Pi has a number of problems. The main one is that the current kernel (or bluez) shipped in Raspbian is broken - as soon as one controller is paired and has an active data link, the bluetooth adapter receives a command telling it to disable connecting to any other devices. Fortunately I was camping with [ms7821](https://twitter.com/marksteward) and [gavan](https://twitter.com/gavanfantom) who are terrifying bluetooth experts and spent a happy afternoon with HCI packet dumps debugging this. [scan_enable.sh](scan_enable.sh) is the hilarious solution they came up with - once a second it pokes all the bluetooth devices and tells them to accept connections again. - -Future Things I May Do ----------------------- -* A pre-built bootable image for the Raspberry Pi (possibly USB bootable) -* Debian packages of PSMoveAPI for the pi -* A script that does all the install things -* Better support for more than 8 controllers diff --git a/oust.py b/oust.py index 339e0b7..75121c9 100644 --- a/oust.py +++ b/oust.py @@ -1,24 +1,66 @@ from __future__ import print_function - +import os +import sys import time -import psmove + +# warn if not running as root +if os.geteuid() != 0: + print("ERROR: You need to run Oust as root so it has Bluetooth access.") + exit(1) + +BASE = os.path.join(os.path.dirname(__file__), '..', 'psmoveapi') + +if 'PSMOVEAPI_LIBRARY_PATH' not in os.environ: + os.environ['PSMOVEAPI_LIBRARY_PATH'] = os.path.join(BASE, 'build') + +sys.path.insert(0, os.path.join(BASE, 'bindings', 'python')) + +import psmoveapi + import subprocess from collections import defaultdict -def disconnect_move(move): - subprocess.call(['hcitool', 'dc', move.get_serial()]) - -poll_failures = defaultdict(int) -def poll_move(move): - serial = move.get_serial() - if move.poll(): - poll_failures[serial] = 0 - return True - else: - poll_failures[serial] += 1 - if poll_failures[serial] > 1000: - print("Disconnecting %s due to poll failure" % move.get_serial()) - disconnect_move(move) +# Track all controller objects by serial number for access during animations +_controller_registry = {} + +def disconnect_move_by_serial(serial): + subprocess.call(['hcitool', 'dc', serial]) + +def pair_controllers(): + """Run the psmove pair utility to pair USB-connected controllers""" + library_path = os.environ.get('PSMOVEAPI_LIBRARY_PATH', os.path.join(os.path.dirname(__file__), '..', 'psmoveapi', 'build')) + pair_binary = os.path.join(library_path, 'psmove') + + if not os.path.exists(pair_binary): + print(f"Warning: psmove binary not found at {pair_binary}") + return False + + print(f"Running pairing utility: {pair_binary} pair") + try: + # Run the pairing command - note this needs sudo privileges + result = subprocess.run(['sudo', pair_binary, 'pair'], + capture_output=True, + text=True, + timeout=10) + + # Print all output (both stdout and stderr) + if result.stdout: + print(result.stdout, end='') + if result.stderr: + print(result.stderr, end='') + + # Check if pairing succeeded by looking for success message in output + success = 'succeeded' in result.stdout.lower() or 'succeeded' in result.stderr.lower() + + if success: + print("Pairing succeeded! Unplug USB and press PS button to connect via Bluetooth") + + return success + except subprocess.TimeoutExpired: + print("Pairing command timed out") + return False + except Exception as e: + print(f"Error running pairing command: {e}") return False # This nightmarish function was taken from stackoverflow @@ -33,217 +75,293 @@ def hsv_to_rgb(h, s, v): if i == 4: return [t, p, v] if i == 5: return [v, p, q] -HSV = colour_range = [] -controller_colours = {} - -def regenerate_colours(): - global HSV, colour_range, controller_colours - HSV = [(x*1.0/len(moves), 1, 1) for x in range(len(moves))] - colour_range = [[int(x) for x in hsv_to_rgb(*colour)] for colour in HSV] - controller_colours = {move.get_serial(): colour_range[i] for i, move in enumerate(moves)} - - -def sleep_controllers(sleep=0.5, leds=(255,255,255), rumble=0, moves=[]): - pause_time = time.time() + sleep - while time.time() < pause_time: - for othermove in moves: - othermove.poll() - othermove.set_rumble(rumble) - othermove.set_leds(*leds) - othermove.update_leds() - - -paired_controllers = [] -controllers_alive = {} -usb_paired_controllers = [] -while True: - start = False - battery = False - battery_pressed = False - while True: - battery_pressed = False - moves = [psmove.PSMove(x) for x in range(psmove.count_connected())] - for move in moves: - if move.this == None: - print("Move initialisation failed, reinitialising") - moves = [] - break - - # If a controller is plugged in over USB, pair it and turn it white - if move.connection_type == psmove.Conn_USB: - #make sure the serial is not None. this might happen if you pull the usb while still in this loop - if move.get_serial() not in usb_paired_controllers and move.get_serial() != None: - move.pair() - usb_paired_controllers.append(move.get_serial()) - print(move.get_serial() + " connected over USB") - move.set_leds(255,255,255) - move.update_leds() - continue - - # If the controller pairs over BT, add it to the list and turn it white - if move.connection_type == psmove.Conn_Bluetooth: - if move.get_serial() not in paired_controllers: - move.pair() - paired_controllers.append(move.get_serial()) - print(move.get_serial() + "connected over bluetooth") - move.set_leds(255,255,255) - move.update_leds() - continue - - if poll_move(move): - # If the trigger is pulled, join the game - if move.get_serial() not in controllers_alive: - if move.get_trigger() > 100: - controllers_alive[move.get_serial()] = move - - if move.get_serial() in controllers_alive: - move.set_leds(255,255,255) - else: - move.set_leds(0,0,0) - - # START starts the game early - if move.get_buttons() == 2048: - start = True - - # Circle shows battery level - if move.get_buttons() == 32: - battery_pressed = True - battery = True - - if battery: - level = move.get_battery() - - if level == 5: # 100% - green - move.set_leds(0, 255, 0) - elif level == 4: # 80% - green-ish - move.set_leds(128, 200, 0) - elif level == 3: # 60% - yellow - move.set_leds(255, 255, 0) - else: # <= 40% - red - move.set_leds(255, 0, 0) - - move.set_rumble(0) - move.update_leds() - - # SELECT disconnects the move - if move.get_buttons() == 256: - disconnect_move(move) - - if battery and not battery_pressed: - battery = False - - # If we've got more/less moves, register them - if psmove.count_connected() != len(moves): - moves = [psmove.PSMove(x) for x in range(psmove.count_connected())] +class OustGame(psmoveapi.PSMoveAPI): + def __init__(self): + super().__init__() + self.quit = False + + # Game state + self.paired_controllers = [] + self.usb_paired_controllers = [] + self.controllers_alive = {} + self.controller_colours = {} + self.poll_failures = defaultdict(int) + + # Game flags + self.in_lobby = True + self.in_game = False + self.in_countdown = False + self.battery_mode = False + self.start_pressed = False + + # Game data + self.move_last_values = {} + + def on_connect(self, controller): + print(f"Controller {controller.serial} connected") + + # If a controller is plugged in over USB, automatically run pairing + if controller.usb: + if controller.serial not in self.usb_paired_controllers: + self.usb_paired_controllers.append(controller.serial) + print(f"{controller.serial} connected over USB - attempting to pair...") + controller.color = psmoveapi.RGB(1.0, 1.0, 1.0) + + # Automatically run the pairing utility + pair_controllers() + + # If the controller connects over BT, add it to the list and turn it white + elif controller.bluetooth: + if controller.serial not in self.paired_controllers: + self.paired_controllers.append(controller.serial) + print(f"{controller.serial} connected over bluetooth") + controller.color = psmoveapi.RGB(1.0, 1.0, 1.0) + + def on_update(self, controller): + # Skip USB controllers (they're just for pairing) + if controller.usb: + controller.color = psmoveapi.RGB(1.0, 1.0, 1.0) + return + + if self.in_lobby: + self.handle_lobby_update(controller) + elif self.in_countdown: + return + elif self.in_game: + self.handle_game_update(controller) + + def on_disconnect(self, controller): + print(f"Controller {controller.serial} disconnected") + if controller.serial in self.controllers_alive: + del self.controllers_alive[controller.serial] + if controller.serial in self.move_last_values: + del self.move_last_values[controller.serial] + + def handle_lobby_update(self, controller): + # If the trigger is pulled, join the game + if controller.serial not in self.controllers_alive: + if controller.trigger > 0.5: # Trigger is 0.0-1.0, so > 0.5 means pulled halfway + self.controllers_alive[controller.serial] = controller + print(f"{controller.serial} joined the game") + + # Set LED based on whether they're in the game + if controller.serial in self.controllers_alive: + controller.color = psmoveapi.RGB(1.0, 1.0, 1.0) + else: + controller.color = psmoveapi.RGB(0.2, 0.1, 0.0) + + # START starts the game early (button value 2048 = START) + if controller.buttons & (1 << 11): # START button + self.start_pressed = True + + # Circle shows battery level (button value 32 = CIRCLE) + if controller.buttons & (1 << 5): # CIRCLE button + self.battery_mode = True + level = controller.battery + + if level == 5: # 100% - green + controller.color = psmoveapi.RGB(0.0, 1.0, 0.0) + elif level == 4: # 80% - green-ish + controller.color = psmoveapi.RGB(0.5, 0.78, 0.0) + elif level == 3: # 60% - yellow + controller.color = psmoveapi.RGB(1.0, 1.0, 0.0) + else: # <= 40% - red + controller.color = psmoveapi.RGB(1.0, 0.0, 0.0) + else: + if self.battery_mode: + self.battery_mode = False + + controller.rumble = 0 + + # SELECT or PS disconnects the controller + if controller.buttons & (1 << 8) or controller.buttons & (1 << 16): # SELECT or PS button + disconnect_move_by_serial(controller.serial) + + def handle_game_update(self, controller): + # Only process controllers that are alive in the game + if controller.serial not in self.controllers_alive: + controller.color = psmoveapi.RGB(0.0, 0.0, 0.0) + return + + # Get accelerometer data + ax = controller.accelerometer.x + ay = controller.accelerometer.y + az = controller.accelerometer.z + total = abs(ax) + abs(ay) + abs(az) + + if controller.serial in self.move_last_values: + change = abs(self.move_last_values[controller.serial] - total) + + # Dead + if change > 0.7: + print(f"DEAD {controller.serial}") + controller.color = psmoveapi.RGB(0.0, 0.0, 0.0) + controller.rumble = 100 + del self.controllers_alive[controller.serial] + + # Warn + elif change > 0.2: + r, g, b = self.controller_colours[controller.serial] + controller.color = psmoveapi.RGB(r*0.3/255.0, g*0.3/255.0, b*0.3/255.0) + + # Reset + else: + r, g, b = self.controller_colours[controller.serial] + controller.color = psmoveapi.RGB(r/255.0, g/255.0, b/255.0) + controller.rumble = 0 + + self.move_last_values[controller.serial] = total + + def regenerate_colours(self): + """Generate unique colors for each controller in the game""" + alive_serials = list(self.controllers_alive.keys()) + HSV = [(x*1.0/len(alive_serials), 1, 1) for x in range(len(alive_serials))] + colour_range = [[int(x) for x in hsv_to_rgb(*colour)] for colour in HSV] + self.controller_colours = {serial: colour_range[i] for i, serial in enumerate(alive_serials)} + + def sleep_controllers(self, sleep_time, leds, rumble): + """Sleep while keeping all alive controllers at specific LED/rumble settings""" + pause_time = time.time() + sleep_time + while time.time() < pause_time: + for serial, controller in self.controllers_alive.items(): + controller.color = psmoveapi.RGB(leds[0], leds[1], leds[2]) + controller.rumble = rumble + self.update() # Actually send the color/rumble updates to hardware + time.sleep(0.01) + + def check_game_start_conditions(self): + """Check if we should start the game""" + num_alive = len(self.controllers_alive) + # Everyone's in - if (len(controllers_alive) == len(moves) and len(controllers_alive) > 0): - break - # Someone hit triangle - if (len(controllers_alive) >= 2 and start == True): - break - - print("GAME START") - regenerate_colours() - - alive = controllers_alive.values() - - # White - sleep_controllers(sleep=0.5, leds=(255,255,255), rumble=0, moves=alive) - # White/rumble - sleep_controllers(sleep=0.3, leds=(255,255,255), rumble=100, moves=alive) - # Red/norumble - sleep_controllers(sleep=0.75, leds=(50,0,0), rumble=0, moves=alive) - # Yellow - sleep_controllers(sleep=0.75, leds=(50,75,0), rumble=0, moves=alive) - # Green - sleep_controllers(sleep=0.75, leds=(0,50,0), rumble=0, moves=alive) - - # Individual colours - for serial, move in controllers_alive.items(): - move.set_leds(*controller_colours[move.get_serial()]) - - move_last_values = {} - - running = True - while running: - for serial, move in list(controllers_alive.items()): - - # Win animation / reset - if len(controllers_alive) == 1: - print("WIN", serial) - - HSV = [(x*1.0/50, 0.9, 1) for x in range(50)] - colour_range = [[int(x) for x in hsv_to_rgb(*colour)] for colour in HSV] - - serial, move = list(controllers_alive.items())[0] - pause_time = time.time() + 3 - - # Ensure all other moves are dark - for othermove in moves: - othermove.set_leds(0, 0, 0) - othermove.poll() - othermove.update_leds() - - while time.time() < pause_time: - # Double rainbow - move.set_leds(*colour_range[0]) - move.set_rumble(100) - move.poll() - move.update_leds() - - colour_range.append(colour_range.pop(0)) - - # Everybody vibrates - for othermove in moves: - othermove.set_rumble(100) - othermove.poll() - othermove.update_leds() - - time.sleep(0.01) - - running = False - controllers_alive = {} - break - - # Update each move state - if poll_move(move): - ax, ay, az = move.get_accelerometer_frame(psmove.Frame_SecondHalf) - total = sum([ax, ay, az]) - - if serial in move_last_values: - change = abs(move_last_values[serial] - total) - # Dead - if change > 0.7: - print("DEAD", serial) - move.set_leds(0,0,0) - move.set_rumble(100) - del controllers_alive[serial] - - # Warn - elif change > 0.2: - scaled = [int(v*0.3) for v in controller_colours[move.get_serial()]] - move.set_leds(*scaled) - - # Reset - else: - move.set_leds(*controller_colours[move.get_serial()]) - move.set_rumble(0) - - move.update_leds() - move_last_values[serial] = total - - if running: - # If a controller vanishes during the game, remove it from the game - # to allow others to finish - if psmove.count_connected() != len(moves): - moves = [psmove.PSMove(x) for x in range(psmove.count_connected())] - available = [ m.get_serial() for m in moves] - - for serial, move in controllers_alive.items(): - if serial not in available: - del controllers_alive[serial] - - # Somehow, everyone disconnected - if len(moves) == 0: - running = False + if num_alive >= 2 and num_alive == len(self.paired_controllers): + return True + + # Someone hit START + if num_alive >= 2 and self.start_pressed: + return True + + return False + + def start_game(self): + """Transition from lobby to game""" + print("Game Starting") + self.in_lobby = False + self.in_countdown = True + self.start_pressed = False + + # Generate colors for each player + print("Generating Colours") + self.regenerate_colours() + + # Countdown sequence + print("Countdown Sequence") + + # White + self.sleep_controllers(0.5, (1.0, 1.0, 1.0), 0) + # White/rumble + self.sleep_controllers(0.3, (1.0, 1.0, 1.0), 100) + # Red/no rumble + self.sleep_controllers(0.75, (0.2, 0, 0), 0) + # Yellow + self.sleep_controllers(0.75, (0.2, 0.3, 0), 0) + # Green + self.sleep_controllers(0.75, (0, 0.2, 0), 0) + + # Set individual colours + for serial, controller in self.controllers_alive.items(): + r, g, b = self.controller_colours[serial] + controller.color = psmoveapi.RGB(r/255.0, g/255.0, b/255.0) + + self.in_countdown = False + self.in_game = True + print("Game Start!") + + + def check_game_end_conditions(self): + """Check if the game should end""" + if len(self.controllers_alive) <= 1: + return True + return False + + def end_game(self): + """Handle game end and winner celebration""" + if len(self.controllers_alive) == 1: + winner_serial = list(self.controllers_alive.keys())[0] + winner = self.controllers_alive[winner_serial] + print(f"WIN {winner_serial}") + + # Create rainbow animation + HSV = [(x*1.0/50, 0.9, 1) for x in range(50)] + colour_range = [[int(x) for x in hsv_to_rgb(*colour)] for colour in HSV] + + pause_time = time.time() + 3 + idx = 0 + while time.time() < pause_time: + # Winner gets rainbow + r, g, b = colour_range[idx % len(colour_range)] + winner.color = psmoveapi.RGB(r/255.0, g/255.0, b/255.0) + winner.rumble = 100 + self.update() # Send the color/rumble updates to hardware + idx += 1 + time.sleep(0.01) + + # Reset game state + self.in_game = False + self.in_countdown = False + self.in_lobby = True + self.controllers_alive = {} + self.move_last_values = {} + self.controller_colours = {} + + def run(self): + """Main game loop""" + while not self.quit: + self.update() + + # Check lobby conditions + if self.in_lobby and self.check_game_start_conditions(): + self.start_game() + + # Check game end conditions + if self.in_game and self.check_game_end_conditions(): + self.end_game() + + time.sleep(0.01) + + +if __name__ == '__main__': + print("=" * 6) + print(" OUST ") + print("=" * 6) + print("This game is designed to work unattended with no screen") + print("=" * 60) + print("\n* Pairing a new controller *") + print("1. Plug in PS Move controller via USB") + print("2. Wait for light to turn solid white") + print("3. Unplug USB cable") + print("4. Press the PS button on the controller to connect via Bluetooth") + print("5. Controller lights up dim orange to show it's connected") + print("\n* Button Controls: *") + print("While in the lobby:") + print(" - Pull trigger to join the game") + print(" - Press CIRCLE to show battery level") + print(" - Press SELECT to disconnect controller") + print(" - Press START to start the game early\n") + + + # Unblock bluetooth + try: + print("Unblocking bluetooth using rfkill...") + subprocess.run(['sudo', 'rfkill', 'unblock', 'bluetooth'], check=True) + except subprocess.CalledProcessError as e: + print(f"Warning: Failed to unblock bluetooth: {e}") + except FileNotFoundError: + print("Warning: rfkill command not found") + + print("Let's go!\n") + + game = OustGame() + game.run() + diff --git a/oust.service b/oust.service new file mode 100644 index 0000000..4965356 --- /dev/null +++ b/oust.service @@ -0,0 +1,14 @@ +[Unit] +Description=Oust Game +After=bluetooth.target + +[Service] +User=root +Type=exec +WorkingDirectory=/home/oust/Oust +ExecStart=/usr/bin/python oust.py +Restart=always +RestartSec=60 + +[Install] +WantedBy=multi-user.target diff --git a/scan_enable.sh b/scan_enable.sh deleted file mode 100755 index e997543..0000000 --- a/scan_enable.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -set -eo pipefail - -echo "Watching hci devices..." - -while true; do - devices=$(hciconfig dev | grep hci | awk '{print $1}' | sed -e 's/://') - - for device in $devices - do - # Force the device to be up, bluez 5.0 does not remember device power - # state through restarts - hciconfig "${device}" up - - scan_enable=$(hcitool -i "${device}" cmd 0x3 0x19|grep -A1 'HCI Event'|tail -1|cut -d\ -f7) - if [[ "$scan_enable" == '00' ]]; then - echo "Scan disabled, enabling" - # Could also use hciconfig hci0 pscan - success=$(hcitool -i "${device}" cmd 0x3 0x1a 0x2|grep -A1 'HCI Event'|tail -1|cut -d\ -f6) - if [[ "$success" != '00' ]]; then - echo "Scan enable failed" - break - fi - echo "Scan now enabled for ${device}" - fi - done - - sleep 1 -done diff --git a/supervisor/conf.d/joust.conf b/supervisor/conf.d/joust.conf deleted file mode 100644 index 487f323..0000000 --- a/supervisor/conf.d/joust.conf +++ /dev/null @@ -1,13 +0,0 @@ -[program:scan_enable] -command=/home/pi/scan_enable.sh -user=root -autorestart=true -startretries=1000 - -[program:oust] -command=/usr/bin/python /home/pi/psmoveapi/build/oust.py -directory=/home/pi/psmoveapi/build -environment=HOME="/home/pi" -user=root -autorestart=true -startretries=1000 diff --git a/supervisor/supervisord.conf b/supervisor/supervisord.conf deleted file mode 100644 index 61b3020..0000000 --- a/supervisor/supervisord.conf +++ /dev/null @@ -1,28 +0,0 @@ -; supervisor config file - -[unix_http_server] -file=/var/run//supervisor.sock ; (the path to the socket file) -chmod=0700 ; sockef file mode (default 0700) - -[supervisord] -logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log) -pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid) -childlogdir=/var/log/supervisor ; ('AUTO' child log dir, default $TEMP) - -; the below section must remain in the config file for RPC -; (supervisorctl/web interface) to work, additional interfaces may be -; added by defining them in separate rpcinterface: sections -[rpcinterface:supervisor] -supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface - -[supervisorctl] -serverurl=unix:///var/run//supervisor.sock ; use a unix:// URL for a unix socket - -; The [include] section can just contain the "files" setting. This -; setting can list multiple files (separated by whitespace or -; newlines). It can also contain wildcards. The filenames are -; interpreted as relative to this file. Included files *cannot* -; include files themselves. - -[include] -files = /etc/supervisor/conf.d/*.conf