Dance contest multiplayer handlers

This commit is contained in:
Ben 2020-02-14 21:40:16 +00:00
parent de4946771c
commit da566b2412
3 changed files with 209 additions and 13 deletions

View File

@ -414,8 +414,9 @@ DROP TABLE IF EXISTS dance_song;
CREATE TABLE dance_song ( CREATE TABLE dance_song (
id INT NOT NULL, id INT NOT NULL,
name VARCHAR (30) NOT NULL, name VARCHAR (30) NOT NULL,
song_length INT NOT NULL DEFAULT 100000, song_length_millis INT NOT NULL,
millis_per_bar INT NOT NULL DEFAULT 2000, song_length INT NOT NULL,
millis_per_bar INT NOT NULL,
PRIMARY KEY (id) PRIMARY KEY (id)
); );
@ -423,7 +424,8 @@ COMMENT ON TABLE dance_song IS 'Dance contest multiplayer tracks';
COMMENT ON COLUMN dance_song.id IS 'Unique song ID'; COMMENT ON COLUMN dance_song.id IS 'Unique song ID';
COMMENT ON COLUMN dance_song.name IS 'Name of song'; COMMENT ON COLUMN dance_song.name IS 'Name of song';
COMMENT ON COLUMN dance_song.song_length IS 'Length of song in milliseconds'; COMMENT ON COLUMN dance_song.song_length_millis IS 'Length of song in milliseconds';
COMMENT ON COLUMN dance_song.song_length IS 'Length of song in beats';
COMMENT ON COLUMN dance_song.millis_per_bar IS 'Milliseconds per song note'; COMMENT ON COLUMN dance_song.millis_per_bar IS 'Milliseconds per song note';
CREATE TABLE room_waddle ( CREATE TABLE room_waddle (
@ -11425,13 +11427,13 @@ INSERT INTO redemption_book_word (book_id, page, line, word_number, answer) VALU
(26, 78, 11, 3, 'sunshine'), (26, 78, 11, 3, 'sunshine'),
(26, 78, 8, 4, 'race'); (26, 78, 8, 4, 'race');
INSERT INTO dance_song (id, name, song_length, millis_per_bar) VALUES INSERT INTO dance_song (id, name, song_length_millis, song_length, millis_per_bar) VALUES
(0, 'Penguin Band Boogie', 123000, 2000), (0, 'Penguin Band Boogie', 123000, 241, 2000),
(1, 'The Generic Way', 117000, 2070), (1, 'The Generic Way', 117000, 221, 2070),
(2, 'Epic Win', 124000, 2666), (2, 'Epic Win', 124000, 176, 2666),
(3, 'Lets Bounce', 130000, 1714), (3, 'Lets Bounce', 130000, 288, 1714),
(4, 'Go West', 139000, 2181), (4, 'Go West', 139000, 248, 2181),
(5, 'Patrick''s Jig', 118000, 2790); (5, 'Patrick''s Jig', 118000, 168, 2790);
INSERT INTO character (id, name, gift_id, stamp_id) VALUES INSERT INTO character (id, name, gift_id, stamp_id) VALUES
(1, 'Rockhopper', 9215, 7), (1, 'Rockhopper', 9215, 7),

View File

@ -1,4 +1,4 @@
from houdini.data import db from houdini.data import db, AbstractDataCollection
class DanceSong(db.Model): class DanceSong(db.Model):
@ -6,5 +6,12 @@ class DanceSong(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30), nullable=False) name = db.Column(db.String(30), nullable=False)
song_length = db.Column(db.Integer, nullable=False, server_default=db.text("100000")) song_length_millis = db.Column(db.Integer, nullable=False)
millis_per_bar = db.Column(db.Integer, nullable=False, server_default=db.text("2000")) song_length = db.Column(db.Integer, nullable=False)
millis_per_bar = db.Column(db.Integer, nullable=False)
class DanceSongCollection(AbstractDataCollection):
__model__ = DanceSong
__indexby__ = 'id'
__filterby__ = 'id'

View File

@ -0,0 +1,187 @@
from houdini import handlers
from houdini.handlers import XTPacket
from houdini.data.dance import DanceSongCollection
from houdini.penguin import Penguin
import random
import time
import asyncio
import itertools
from dataclasses import dataclass
@dataclass
class Dancer:
penguin: Penguin
score: int
difficulty: int
class DanceFloor:
Easy = 0
Medium = 1
Difficult = 2
Expert = 3
Capacity = 15
DanceRoomId = 952
def __init__(self, server):
self.server = server
self._queue = {}
self._dancers = {}
self._tracks = itertools.cycle(server.dance_songs.values())
self._queued_track = next(self._tracks)
self._current_track = None
self._next_song_timestamp = 0
async def add_penguin(self, p):
if p.id not in self._queue and p.id not in self._dancers:
if len(self._queue) < DanceFloor.Capacity:
self._queue[p.id] = Dancer(penguin=p, score=0, difficulty=DanceFloor.Easy)
await p.send_xt('gz', 0, self._queued_track.id, self.get_time_to_next_song())
await p.send_xt('jz', 'true', self._queued_track.id, self.get_time_to_next_song())
else:
await p.send_xt('jz', 'false')
async def remove_penguin(self, p):
self._queue.pop(p.id, None)
if p.id in self._dancers:
self._dancers.pop(p.id)
await self.send_xt('zm', self.get_string())
async def next_round(self):
self._current_track = self._queued_track
self._dancers = self._queue
self._queue = {}
song_data = {
difficulty: DanceFloor._get_song_data(
self._current_track.song_length, self._current_track.millis_per_bar, difficulty
) for difficulty in [
DanceFloor.Easy, DanceFloor.Medium, DanceFloor.Difficult, DanceFloor.Expert
]}
for dancer in self._dancers.values():
note_types, note_times, note_lengths = song_data[dancer.difficulty]
await dancer.penguin.send_xt(
'sz',
','.join(map(str, note_times)),
','.join(map(str, note_types)),
','.join(map(str, note_lengths)),
self._current_track.millis_per_bar
)
await self.send_xt('zm', self.get_string())
self._queued_track = next(self._tracks)
self._next_song_timestamp = int(round(time.time() * 1000)) + self._current_track.song_length_millis
await asyncio.sleep(self._current_track.song_length_millis // 1000)
await self.next_round()
async def send_xt(self, *data):
for dancer in self._dancers.values():
await dancer.penguin.send_xt(*data)
def set_difficulty(self, p, difficulty):
self._queue[p.id].difficulty = max(0, min(difficulty, DanceFloor.Expert))
def set_score(self, p, score):
self._dancers[p.id].score = max(self._dancers[p.id].score, score)
def get_string(self):
return ','.join(f'-1|{dancer.penguin.safe_name}|{dancer.score}' for dancer in self._dancers.values())
def get_time_to_next_song(self):
return self._next_song_timestamp - int(round(time.time() * 1000))
@classmethod
def _get_song_data(cls, song_length, millis_per_bar, difficulty):
millis_per_beat = millis_per_bar // 4
note_types = []
note_times = []
note_lengths = []
last_note_times = [0, 0, 0, 0]
def add_note(beat_time, max_length):
note_type = random.randrange(4)
note_time = int(beat_time * millis_per_beat)
note_length = int(random.randint(0, max_length) * millis_per_beat)
if note_time > last_note_times[note_type]:
note_types.append(note_type)
note_times.append(note_time)
note_lengths.append(note_length)
last_note_times[note_type] = note_time + note_length
for song_time in range(4, song_length):
if not song_time % 8 and difficulty >= cls.Easy:
add_note(song_time, 4)
if random.randrange(2) == 0:
add_note(song_time, 0)
elif not song_time % 4 and difficulty >= cls.Medium:
add_note(song_time, 4)
if random.randrange(4) == 0:
add_note(song_time, 0)
elif not song_time % 2 and difficulty >= cls.Difficult:
add_note(song_time, 2)
elif random.randrange(4) > 0 and difficulty >= cls.Expert:
add_note(song_time, 0)
if random.randrange(4) == 0:
add_note(song_time + 0.5, 0)
return note_types, note_times, note_lengths
@handlers.boot
async def songs_load(server):
server.dance_songs = await DanceSongCollection.get_collection()
server.logger.info(f'Loaded {len(server.dance_songs)} dance tracks')
server.dance_floor = DanceFloor(server)
asyncio.create_task(server.dance_floor.next_round())
@handlers.handler(XTPacket('gz', ext='z'))
@handlers.player_in_room(DanceFloor.DanceRoomId)
async def handle_get_game(p):
await p.server.dance_floor.add_penguin(p)
@handlers.handler(XTPacket('zr', ext='z'))
@handlers.player_in_room(DanceFloor.DanceRoomId)
async def handle_get_game_again(p):
await p.server.dance_floor.add_penguin(p)
@handlers.handler(XTPacket('zd', ext='z'))
@handlers.player_in_room(DanceFloor.DanceRoomId)
async def handle_change_difficulty(p, difficulty: int):
p.server.dance_floor.set_difficulty(p, difficulty)
@handlers.handler(XTPacket('zm', ext='z'))
@handlers.player_in_room(DanceFloor.DanceRoomId)
async def handle_send_move(p, score: int):
p.server.dance_floor.set_score(p, score)
await p.server.dance_floor.send_xt('zm', p.server.dance_floor.get_string())
@handlers.handler(XTPacket('cz', ext='z'))
@handlers.player_in_room(DanceFloor.DanceRoomId)
async def handle_leave_game(p):
await p.server.dance_floor.remove_penguin(p)
@handlers.disconnected
@handlers.player_attribute(joined_world=True)
async def handle_disconnect_dance_floor(p):
await p.server.dance_floor.remove_penguin(p)