Merge pull request #193 from Chroma-Case/front/play-page-connection

This commit is contained in:
Arthur Jamet
2023-04-24 17:29:17 +01:00
committed by GitHub
21 changed files with 587 additions and 331 deletions

View File

@@ -44,7 +44,6 @@ services:
context: ./front
dockerfile: Dockerfile.dev
environment:
- API_URL=http://back:3000/
- SCOROMETER_URL=http://scorometer:6543/
- NGINX_PORT=80
ports:

View File

@@ -10,7 +10,7 @@ import Constants from "expo-constants";
import store from "./state/Store";
import { Platform } from "react-native";
import { en } from "./i18n/Translations";
import { useQuery, QueryClient } from "react-query";
import { QueryClient } from "react-query";
type AuthenticationInput = { username: string; password: string };
type RegistrationInput = AuthenticationInput & { email: string };
@@ -370,15 +370,10 @@ export default class API {
/**
* Retrieve the authenticated user's play history
*/
public static async getUserPlayHistory(): Promise<Song[]> {
const queryClient = new QueryClient();
let songs = await queryClient.fetchQuery(
["API", "allsongs"],
API.getAllSongs
);
const shuffled = [...songs].sort(() => 0.5 - Math.random());
return shuffled.slice(0, 3);
public static async getUserPlayHistory(): Promise<SongHistory[]> {
return this.fetch({
route: '/history'
});
}
/**

View File

@@ -12,6 +12,7 @@ export const en = {
searchBtn: 'Search',
play: 'Play',
playBtn: 'Play',
practiceBtn: 'Practice',
playAgain: 'Play Again',
songPageBtn: 'Go to song page',
level: 'Level',
@@ -97,6 +98,17 @@ export const en = {
errAlrdExst: 'Already exist',
errIncrrct: 'Incorrect Credentials',
// Playback messages
missed: 'Missed note',
perfect: 'Perfect',
great: 'Great',
good: 'Good',
wrong: 'Wrong',
short: 'A little too short',
long: 'A little too long',
tooLong: 'Too Long',
tooShort: 'Too Short',
changePassword: 'Change password',
oldPassword: 'Old password',
@@ -158,6 +170,7 @@ export const fr: typeof en = {
changeLanguageBtn: 'Changer la langue',
searchBtn: 'Rechercher',
playBtn: 'Jouer',
practiceBtn: 'S\'entrainer',
playAgain: 'Rejouer',
songPageBtn: 'Aller sur la page de la chanson',
level: 'Niveau',
@@ -256,6 +269,16 @@ export const fr: typeof en = {
unknownError: 'Erreur inconnue',
errIncrrct: 'Identifiant incorrect',
// Playback messages
missed: 'Raté',
perfect: 'Parfait',
great: 'Super',
good: 'Bien',
wrong: 'Oups',
short: 'Un peu court',
long: 'Un peu long',
tooLong: 'Trop long',
tooShort: 'Trop court',
passwordUpdated: 'Mot de passe mis à jour',
emailUpdated: 'Email mis à jour',
@@ -319,6 +342,7 @@ export const sp: typeof en = {
changeLanguageBtn: 'Cambiar el idioma',
searchBtn: 'Buscar',
playBtn: 'reproducir',
practiceBtn: 'Práctica',
playAgain: 'Repetición',
precisionScore: 'Précision',
songPageBtn: 'canción',
@@ -402,6 +426,16 @@ export const sp: typeof en = {
errAlrdExst: "Ya existe",
errIncrrct: "credenciales incorrectas",
// Playback messages
missed: 'Te perdiste una nota',
perfect: 'Perfecto',
great: 'Excelente',
good: 'Bueno',
wrong: 'Equivocado',
short: 'Un poco demasiado corto',
long: 'Un poco demasiado largo',
tooLong: 'Demasiado largo',
tooShort: 'Demasiado corto',
changePassword: 'Cambio de contraseña',
oldPassword: 'Contraseña anterior',
newPassword: 'Nueva contraseña',

View File

@@ -1,7 +1,8 @@
interface LessonHistory {
songId: number;
userId: number;
interface SongHistory {
songID: number;
userID: number;
score: number;
difficulties: JSON;
}
export default LessonHistory;
export default SongHistory;

View File

@@ -56,6 +56,7 @@
"react-native-web": "~0.18.7",
"react-redux": "^8.0.2",
"react-timer-hook": "^3.0.5",
"react-use-precision-timer": "^3.3.1",
"redux-persist": "^6.0.0",
"soundfont-player": "^0.12.0",
"type-fest": "^3.6.0",

View File

@@ -10,6 +10,7 @@ import CompetenciesTable from '../components/CompetenciesTable'
import ProgressBar from "../components/ProgressBar";
import Translate from "../components/Translate";
import TextButton from "../components/TextButton";
import Song from "../models/Song";
const HomeView = () => {
const navigation = useNavigation();
@@ -19,9 +20,20 @@ const HomeView = () => {
const searchHistoryQuery = useQuery(['history', 'search'], () => API.getSearchHistory());
const skillsQuery = useQuery(['skills'], () => API.getUserSkills());
const nextStepQuery = useQuery(['user', 'recommendations'], () => API.getUserRecommendations());
const artistsQueries = useQueries((playHistoryQuery.data?.concat(searchHistoryQuery.data ?? []).concat(nextStepQuery.data ?? []) ?? []).map((song) => (
{ queryKey: ['artist', song.id], queryFn: () => API.getArtist(song.id) }
)));
const songHistory = useQueries(
playHistoryQuery.data?.map(({ songID }) => ({
queryKey: ['song', songID],
queryFn: () => API.getSong(songID)
})) ?? []
);
const artistsQueries = useQueries((songHistory
.map((entry) => entry.data)
.concat(nextStepQuery.data ?? [])
.filter((s): s is Song => s !== undefined))
.map((song) => (
{ queryKey: ['artist', song.id], queryFn: () => API.getArtist(song.id) }
))
);
if (!userQuery.data || !skillsQuery.data || !searchHistoryQuery.data || !playHistoryQuery.data) {
return <Center style={{ flexGrow: 1 }}>
@@ -67,7 +79,11 @@ const HomeView = () => {
<Box flex={{ md: 1 }}>
<SongCardGrid
heading={<Translate translationKey='recentlyPlayed'/>}
songs={playHistoryQuery.data?.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId))
songs={songHistory
.filter((songQuery) => songQuery.data)
.map(({ data }) => data)
.filter((song, i, array) => array.map((s) => s.id).findIndex((id) => id == song.id) == i)
.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId))
.map((song) => ({
albumCover: song.cover,
songTitle: song.name,

View File

@@ -4,21 +4,25 @@ import * as ScreenOrientation from 'expo-screen-orientation';
import { Box, Center, Column, Progress, Text, Row, View, useToast, Icon } from 'native-base';
import IconButton from '../components/IconButton';
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { useNavigation } from "../Navigation";
import { useNavigation, RouteProps } from "../Navigation";
import { useQuery, useQueryClient } from 'react-query';
import API from '../API';
import LoadingComponent from '../components/Loading';
import Constants from 'expo-constants';
import { useStopwatch } from 'react-timer-hook';
import SlideView from '../components/PartitionVisualizer/SlideView';
import MidiPlayer from 'midi-player-js';
import SoundFont from 'soundfont-player';
import VirtualPiano from '../components/VirtualPiano/VirtualPiano';
import { strToKey, keyToStr, Note } from '../models/Piano';
import { useSelector } from 'react-redux';
import { RootState } from '../state/Store';
import { translate } from '../i18n/i18n';
import { ColorSchemeType } from 'native-base/lib/typescript/components/types';
import { useStopwatch } from "react-use-precision-timer";
type PlayViewProps = {
songId: number
songId: number,
type: 'practice' | 'normal'
}
@@ -33,42 +37,52 @@ if (process.env.NODE_ENV != 'development' && Platform.OS === 'web') {
}
}
const PlayView = ({ songId }: RouteProps<PlayViewProps>) => {
const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
const accessToken = useSelector((state: RootState) => state.user.accessToken);
const navigation = useNavigation();
const queryClient = useQueryClient();
const song = useQuery(['song', songId], () => API.getSong(songId));
const toast = useToast();
const webSocket = useRef<WebSocket>();
const timer = useStopwatch({ autoStart: false });
const [paused, setPause] = useState<boolean>(true);
const stopwatch = useStopwatch();
const [midiPlayer, setMidiPlayer] = useState<MidiPlayer.Player>();
const [isVirtualPianoVisible, setVirtualPianoVisible] = useState<boolean>(false);
const [time, setTime] = useState(0);
const [score, setScore] = useState(0); // Between 0 and 100
const partitionRessources = useQuery(["partition", songId], () =>
API.getPartitionRessources(songId)
);
const onPause = () => {
timer.pause();
midiPlayer?.pause();
stopwatch.pause();
setPause(true);
webSocket.current?.send(JSON.stringify({
type: "pause",
paused: true,
time: Date.now()
time: time
}));
}
const onResume = () => {
if (stopwatch.isStarted()) {
stopwatch.resume();
} else {
stopwatch.start();
}
setPause(false);
midiPlayer?.play();
timer.start();
webSocket.current?.send(JSON.stringify({
type: "pause",
paused: false,
time: Date.now()
time: time
}));
}
const onEnd = () => {
webSocket.current?.send(JSON.stringify({
type: "end"
}));
stopwatch.stop();
webSocket.current?.close();
midiPlayer?.pause();
}
@@ -86,19 +100,55 @@ const PlayView = ({ songId }: RouteProps<PlayViewProps>) => {
webSocket.current.onopen = () => {
webSocket.current!.send(JSON.stringify({
type: "start",
name: "clair-de-lune" /*song.data.id*/,
id: song.data!.id,
mode: type,
bearer: accessToken
}));
};
webSocket.current.onmessage = (message) => {
try {
const data = JSON.parse(message.data);
if (data.type == 'end') {
navigation.navigate('Score');
} else if (data.song_launched == undefined) {
toast.show({ description: data, placement: 'top', colorScheme: 'secondary' });
navigation.navigate('Score', { songId: song.data!.id });
return;
}
} catch {
const points = data.info.score;
const maxPoints = data.info.maxScore;
setScore(Math.floor(Math.max(points, 0) / maxPoints) * 100);
let formattedMessage = '';
let messageColor: ColorSchemeType | undefined;
if (data.type == 'miss') {
formattedMessage = translate('missed');
messageColor = 'black';
} else if (data.type == 'timing' || data.type == 'duration') {
formattedMessage = translate(data[data.type]);
switch (data[data.type]) {
case 'perfect':
messageColor = 'fuchsia';
break;
case 'great':
messageColor = 'green';
break;
case 'short':
case 'long':
case 'good':
messageColor = 'lightBlue';
break;
case 'too short':
case 'too long':
case 'wrong':
messageColor = 'grey';
break;
default:
break;
}
}
toast.show({ description: formattedMessage, placement: 'top', colorScheme: messageColor ?? 'secondary' });
} catch (e) {
console.log(e);
}
}
inputs.forEach((input) => {
@@ -110,9 +160,9 @@ const PlayView = ({ songId }: RouteProps<PlayViewProps>) => {
const keyCode = message.data[1];
webSocket.current?.send(JSON.stringify({
type: keyIsPressed ? "note_on" : "note_off",
node: keyCode,
intensity: null,
time: Date.now()
note: keyCode,
id: song.data!.id,
time: time
}))
}
inputIndex++;
@@ -123,7 +173,6 @@ const PlayView = ({ songId }: RouteProps<PlayViewProps>) => {
]).then(([midiFile, audioController]) => {
const player = new MidiPlayer.Player((event) => {
if (event['noteName']) {
console.log(event);
audioController.play(event['noteName']);
}
});
@@ -134,17 +183,23 @@ const PlayView = ({ songId }: RouteProps<PlayViewProps>) => {
const onMIDIFailure = () => {
toast.show({ description: `Failed to get MIDI access` });
}
useEffect(() => {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE).catch(() => {});
navigator.requestMIDIAccess().then(onMIDISuccess, onMIDIFailure);
let interval = setInterval(() => setTime(() => stopwatch.getElapsedRunningTime()), 1);
return () => {
ScreenOrientation.unlockAsync().catch(() => {});
clearInterval(timer);
onEnd();
clearInterval(interval);
}
}, [])
const score = 20;
}, []);
useEffect(() => {
if (song.data) {
navigator.requestMIDIAccess().then(onMIDISuccess, onMIDIFailure);
}
}, [song.data]);
if (!song.data || !partitionRessources.data) {
return <Center style={{ flexGrow: 1 }}>
@@ -219,12 +274,12 @@ const PlayView = ({ songId }: RouteProps<PlayViewProps>) => {
} onPress={() => {
setVirtualPianoVisible(!isVirtualPianoVisible);
}}/>
<Text>{timer.minutes}:{timer.seconds.toString().padStart(2, '0')}</Text>
<Text>{Math.floor(time / 60000)}:{Math.floor((time % 60000) / 1000).toFixed(0).toString().padStart(2, '0')}</Text>
<IconButton size='sm' colorScheme='coolGray' variant='solid' icon={
<Icon as={Ionicons} name="stop"/>
} onPress={() => {
onEnd();
navigation.navigate('Score')
navigation.navigate('Score', { songId: song.data.id });
}}/>
</Row>
</Row>

View File

@@ -10,35 +10,40 @@ import LoadingComponent from "../components/Loading";
type ScoreViewProps = { songId: number }
const ScoreView = ({ songId }: RouteProps<ScoreViewProps>) => {
const ScoreView = ({ songId, route }: RouteProps<ScoreViewProps>) => {
const theme = useTheme();
const navigation = useNavigation();
// const songQuery = useQuery(['song', props.songId], () => API.getSong(props.songId));
// const songScoreQuery = useQuery(['song', props.songId, 'score', 'latest'], () => API.getLastSongPerformanceScore(props.songId));
const songQuery = useQuery(['song', songId], () => API.getSong(songId));
const artistQuery = useQuery(['song', songId],
() => API.getArtist(songQuery.data!.artistId!),
{ enabled: songQuery.data != undefined }
);
const songScoreQuery = useQuery(["score", songId], () => API.getUserPlayHistory()
.then((history) => history.find((h) => h.songID == songId )!));
// const perfoamnceRecommandationsQuery = useQuery(['song', props.songId, 'score', 'latest', 'recommendations'], () => API.getLastSongPerformanceScore(props.songId));
const recommendations = useQuery(['song', 'recommendations'], () => API.getUserRecommendations());
if (!recommendations.data) {
if (!recommendations.data || !songScoreQuery.data || !songQuery.data || (songQuery.data.artistId && !artistQuery.data)) {
return <Center style={{ flexGrow: 1 }}>
<LoadingComponent/>
</Center>;
}
return <ScrollView p={8} contentContainerStyle={{ alignItems: 'center' }}>
<VStack width={{ base: '100%', lg: '50%' }} textAlign='center'>
<Text bold fontSize='lg'>Rolling in the Deep</Text>
<Text bold>Adele - 3:45</Text>
<Text bold fontSize='lg'>{songQuery.data.name}</Text>
<Text bold>{artistQuery.data?.name}</Text>
<Row style={{ justifyContent: 'center', display: 'flex' }}>
<Card shadow={3} style={{ flex: 1 }}>
<Image
style={{ zIndex: 0, aspectRatio: 1, margin: 5, borderRadius: CardBorderRadius}}
source={{ uri: 'https://imgs.search.brave.com/AinqAz0knOSOt0V3rcv7ps7aMVCo0QQfZ-1NTdwVjK0/rs:fit:1200:1200:1/g:ce/aHR0cDovLzEuYnAu/YmxvZ3Nwb3QuY29t/Ly0xTmZtZTdKbDVk/US9UaHd0Y3pieEVa/SS9BQUFBQUFBQUFP/TS9QdGx6ZWtWd2Zt/ay9zMTYwMC9BZGVs/ZSstKzIxKyUyNTI4/T2ZmaWNpYWwrQWxi/dW0rQ292ZXIlMjUy/OS5qcGc' }}
source={{ uri: songQuery.data.cover }}
/>
</Card>
<Card shadow={3} style={{ flex: 1 }}>
<Column style={{ justifyContent: 'space-evenly', flexGrow: 1 }}>
<Row style={{ alignItems: 'center' }}>
{/*<Row style={{ alignItems: 'center' }}>
<Text bold fontSize='xl'>
80
</Text>
<Translate translationKey='goodNotes' format={(t) => ' ' + t}/>
</Row>
@@ -47,15 +52,14 @@ const ScoreView = ({ songId }: RouteProps<ScoreViewProps>) => {
80
</Text>
<Translate translationKey='goodNotesInARow' format={(t) => ' ' + t}/>
</Row>
</Row>*/}
<Row style={{ alignItems: 'center' }}>
<Translate translationKey='precisionScore' format={(t) => t + ' : '}/>
<Translate translationKey='score' format={(t) => t + ' : '}/>
<Text bold fontSize='xl'>
{"80" + "%"}
{songScoreQuery.data.score + "pts"}
</Text>
</Row>
</Column>
{/* Precision */}
</Card>
</Row>
<SongCardGrid
@@ -76,7 +80,7 @@ const ScoreView = ({ songId }: RouteProps<ScoreViewProps>) => {
onPress={() => navigation.navigate('Home')}
/>
<TextButton
onPress={() => navigation.navigate('Song', { songId: 1 })}
onPress={() => navigation.navigate('Song', { songId })}
translate={{ translationKey: 'playAgain' }}
/>
</Row>

View File

@@ -1,4 +1,4 @@
import { Button, Divider, Box, Center, Image, Text, VStack, PresenceTransition, Icon } from "native-base";
import { Divider, Box, Center, Image, Text, VStack, PresenceTransition, Icon, Stack } from "native-base";
import { useQuery } from 'react-query';
import LoadingComponent from "../components/Loading";
import React, { useEffect, useState } from "react";
@@ -7,7 +7,7 @@ import formatDuration from "format-duration";
import { Ionicons } from '@expo/vector-icons';
import API from "../API";
import TextButton from "../components/TextButton";
import { useNavigation } from "../Navigation";
import { useNavigation, RouteProps } from "../Navigation";
interface SongLobbyProps {
// The unique identifier to find a song
@@ -37,7 +37,7 @@ const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
</Box>
<Box style={{ flex: 0.5 }}/>
<Box style={{ flex: 3, padding: 10, flexDirection: 'column', justifyContent: 'space-between' }}>
<Box flex={1}>
<Stack flex={1} space={3}>
<Text bold isTruncated numberOfLines={2} fontSize='lg'>{songQuery.data!.name}</Text>
<Text>
<Translate translationKey='level'
@@ -45,10 +45,15 @@ const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
/>
</Text>
<TextButton translate={{ translationKey: 'playBtn' }} width='auto'
onPress={() => navigation.navigate('Play', { songId: songQuery.data!.id })}
onPress={() => navigation.navigate('Play', { songId: songQuery.data!.id, type: 'normal' })}
rightIcon={<Icon as={Ionicons} name="play-outline"/>}
/>
</Box>
<TextButton translate={{ translationKey: 'practiceBtn' }} width='auto'
onPress={() => navigation.navigate('Play', { songId: songQuery.data!.id, type: 'practice' })}
rightIcon={<Icon as={Ionicons} name="play-outline"/>}
colorScheme='secondary'
/>
</Stack>
</Box>
</Box>
<Box style={{ flexDirection: 'row', justifyContent: 'space-between', padding: 30}}>

View File

@@ -14939,6 +14939,11 @@ react-shallow-renderer@^16.13.1, react-shallow-renderer@^16.15.0:
object-assign "^4.1.1"
react-is "^16.12.0 || ^17.0.0 || ^18.0.0"
react-sub-unsub@^2.1.6:
version "2.1.11"
resolved "https://registry.yarnpkg.com/react-sub-unsub/-/react-sub-unsub-2.1.11.tgz#173d0803e1d7b29611cb29d95f47ed7798e93642"
integrity sha512-FNKy0uD5wSieRE+l5RXaS0bUu6cR8XAXLDwOJnvSDGBMHcWVb1dod8ZkXYjPKtKR74tjYCEpMWcEAWCOoWNXxQ==
react-test-renderer@17.0.2, react-test-renderer@~17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c"
@@ -14954,6 +14959,13 @@ react-timer-hook@^3.0.5:
resolved "https://registry.yarnpkg.com/react-timer-hook/-/react-timer-hook-3.0.5.tgz#a8d930f99b180cd88da245965a26a17df3e7457b"
integrity sha512-n+98SdmYvui2ne3KyWb3Ldu4k0NYQa3g/VzW6VEIfZJ8GAk/jJsIY700M8Nd2vNSTj05c7wKyQfJBqZ0x7zfiA==
react-use-precision-timer@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/react-use-precision-timer/-/react-use-precision-timer-3.3.1.tgz#8e49b6f58d507647925bf633a673a7cb0b2b924d"
integrity sha512-PUCpFp48ftKoV2C+hz57mbqzqojE/Ol169Lyk2fFEIapsOH6tKIis8vZwmloedRe916qmJCOkXp+h9IB6QJY+A==
dependencies:
react-sub-unsub "^2.1.6"
react@18.1.0:
version "18.1.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890"

View File

@@ -0,0 +1,78 @@
import json
from dataclasses import dataclass
from typing import Literal, Tuple
from validated_dc import ValidatedDC, get_errors, is_valid
@dataclass
class InvalidMessage:
message: str
@dataclass
class StartMessage(ValidatedDC):
id: int
bearer: str
mode: Literal["normal", "practice"]
type: Literal["start"] = "start"
@dataclass
class EndMessage(ValidatedDC):
type: Literal["end"] = "end"
@dataclass
class NoteOnMessage(ValidatedDC):
time: int
note: int
id: int
type: Literal["note_on"] = "note_on"
@dataclass
class NoteOffMessage(ValidatedDC):
time: int
note: int
id: int
type: Literal["note_off"] = "note_off"
@dataclass
class PauseMessage(ValidatedDC):
paused: bool
time: int
type: Literal["pause"] = "pause"
message_map = {
"start": StartMessage,
"end": EndMessage,
"note_on": NoteOnMessage,
"note_off": NoteOffMessage,
"pause": PauseMessage,
}
def getMessage() -> (
Tuple[
StartMessage
| EndMessage
| NoteOnMessage
| NoteOffMessage
| PauseMessage
| InvalidMessage,
str,
]
):
try:
msg = input()
obj = json.loads(msg)
res = message_map[obj["type"]](**obj)
if is_valid(res):
return res, msg
else:
return InvalidMessage(str(get_errors(res))), msg
except Exception as e:
return InvalidMessage(str(e)), ""

View File

@@ -4,16 +4,22 @@ import json
import logging
import operator
import os
import select
import sys
from dataclasses import dataclass
from typing import Literal, Tuple
from typing import TypedDict
import requests
from chroma_case.Key import Key
from chroma_case.Message import (
EndMessage,
InvalidMessage,
NoteOffMessage,
NoteOnMessage,
PauseMessage,
StartMessage,
getMessage,
)
from chroma_case.Partition import Partition
from mido import MidiFile
from validated_dc import ValidatedDC, get_errors, is_valid
BACK_URL = os.environ.get("BACK_URL") or "http://back:3000"
MUSICS_FOLDER = os.environ.get("MUSICS_FOLDER") or "/musics/"
@@ -27,77 +33,13 @@ NORMAL = 0
PRACTICE = 1
@dataclass
class InvalidMessage:
message: str
@dataclass
class StartMessage(ValidatedDC):
id: int
bearer: str
mode: Literal["normal", "practice"]
type: Literal["start"] = "start"
@dataclass
class EndMessage(ValidatedDC):
type: Literal["end"] = "end"
@dataclass
class NoteOnMessage(ValidatedDC):
time: int
note: int
id: int
type: Literal["note_on"] = "note_on"
@dataclass
class NoteOffMessage(ValidatedDC):
time: int
note: int
id: int
type: Literal["note_off"] = "note_off"
@dataclass
class PauseMessage(ValidatedDC):
paused: bool
time: int
type: Literal["pause"] = "pause"
message_map = {
"start": StartMessage,
"end": EndMessage,
"note_on": NoteOnMessage,
"note_off": NoteOffMessage,
"pause": PauseMessage,
}
def getMessage() -> (
Tuple[
StartMessage
| EndMessage
| NoteOnMessage
| NoteOffMessage
| PauseMessage
| InvalidMessage,
str,
]
):
try:
msg = input()
obj = json.loads(msg)
res = message_map[obj["type"]](**obj)
if is_valid(res):
return res, msg
else:
return InvalidMessage(str(get_errors(res))), msg
except Exception as e:
return InvalidMessage(str(e)), ""
class ScoroInfo(TypedDict):
max_score: int
score: int
missed: int
perfect: int
great: int
good: int
def send(o):
@@ -105,30 +47,29 @@ def send(o):
class Scorometer:
def __init__(self, mode, midiFile, song_id, user_id) -> None:
self.partition = self.getPartition(midiFile)
def __init__(self, mode: int, midiFile: str, song_id: int, user_id: int) -> None:
self.partition: Partition = self.getPartition(midiFile)
self.practice_partition: list[list[Key]] = self.getPracticePartition(mode)
self.keys_down = []
self.mode = mode
self.song_id = song_id
self.user_id = user_id
self.score = 0
self.missed = 0
self.perfect = 0
self.great = 0
self.good = 0
self.mode: int = mode
self.song_id: int = song_id
self.user_id: int = user_id
self.wrong_ids = []
self.difficulties = {}
if mode == PRACTICE:
get_start = operator.attrgetter("start")
self.practice_partition = [
list(g)
for _, g in itertools.groupby(
sorted(self.partition.notes, key=get_start), get_start
)
]
else:
self.practice_partition: list[list[Key]] = []
self.info: ScoroInfo = {
"max_score": len(self.partition.notes) * 100,
"score": 0,
"missed": 0,
"perfect": 0,
"great": 0,
"good": 0,
}
def getPartition(self, midiFile):
def send(self, obj):
obj["info"] = self.info
send(obj)
def getPartition(self, midiFile: str):
notes = []
s = 3500
notes_on = {}
@@ -150,21 +91,26 @@ class Scorometer:
notes_on[d["note"]] = s # 500
return Partition(midiFile, notes)
def handleNoteOn(self, message: NoteOnMessage):
_key = message.note
timestamp = message.time
is_down = any(x[0] == _key for x in self.keys_down)
if not is_down:
self.keys_down.append((_key, timestamp))
logging.debug({"note": _key})
def getPracticePartition(self, mode: int) -> list[list[Key]]:
get_start = operator.attrgetter("start")
return (
[
list(g)
for _, g in itertools.groupby(
sorted(self.partition.notes, key=get_start), get_start
)
]
if mode == PRACTICE
else []
)
def handleNoteOff(self, message: NoteOffMessage):
_key = message.note
timestamp = message.time
down_since = next(since for (h_key, since) in self.keys_down if h_key == _key)
self.keys_down.remove((_key, down_since))
key = Key(_key, down_since, (timestamp - down_since))
# debug({key: key})
def handleNoteOn(self, message: NoteOnMessage):
is_down = any(x[0] == message.note for x in self.keys_down)
logging.debug({"note_on": message.note})
if is_down:
return
self.keys_down.append((message.note, message.time))
key = Key(key=message.note, start=message.time, duration=0)
to_play = next(
(
i
@@ -173,82 +119,129 @@ class Scorometer:
),
None,
)
if to_play is None:
self.score -= 50
logging.info("Invalid key.")
if to_play:
perf = self.getTimingScore(key, to_play)
logging.debug({"note_on": f"{perf} on {message.note}"})
self.send({"type": "timing", "id": message.id, "timing": perf})
else:
timingScore, timingInformation = self.getTiming(key, to_play)
self.score += (
self.info["score"] -= 50
self.wrong_ids += [message.id]
logging.debug({"note_on": f"wrong key {message.note}"})
self.send({"type": "timing", "id": message.id, "timing": "wrong"})
def handleNoteOff(self, message: NoteOffMessage):
logging.debug({"note_off": message.note})
down_since = next(
since for (h_key, since) in self.keys_down if h_key == message.note
)
self.keys_down.remove((message.note, down_since))
if message.id in self.wrong_ids:
logging.debug({"note_off": f"wrong key {message.note}"})
self.send({"type": "duration", "id": message.id, "duration": "wrong"})
return
key = Key(
key=message.note, start=down_since, duration=(message.time - down_since)
)
to_play = next(
(
i
for i in self.partition.notes
if i.key == key.key and self.is_timing_close(key, i) and i.done is False
),
None,
)
if to_play:
perf = self.getDurationScore(key, to_play)
self.info["score"] += (
100
if timingScore == "perfect"
if perf == "perfect"
else 75
if timingScore == "great"
if perf == "short" or perf == "long"
else 50
)
to_play.done = True
self.sendScore(message.id, timingScore, timingInformation)
logging.debug({"note_off": f"{perf} on {message.note}"})
self.send({"type": "duration", "id": message.id, "duration": perf})
else:
logging.warning("note_off: no key to play but it was not a wrong note_on")
def handleNoteOnPractice(self, message: NoteOnMessage):
_key = message.note
timestamp = message.time
is_down = any(x[0] == _key for x in self.keys_down)
if not is_down:
self.keys_down.append((_key, timestamp))
logging.debug({"note": _key})
def handleNoteOffPractice(self, message: NoteOffMessage):
_key = message.note
timestamp = message.time
# is_down = any(x[0] == _key for x in self.keys_down)
down_since = next(since for (h_key, since) in self.keys_down if h_key == _key)
self.keys_down.remove((_key, down_since))
key = Key(_key, down_since, (timestamp - down_since))
is_down = any(x[0] == message.note for x in self.keys_down)
logging.debug({"note_on": message.note})
if is_down:
return
self.keys_down.append((message.note, message.time))
key = Key(key=message.note, start=message.time, duration=0)
keys_to_play = next(
(i for i in self.practice_partition if any(x.done is not True for x in i)),
None,
)
if keys_to_play is None:
logging.info("Key sent but there is no keys to play")
self.score -= 50
self.send({"type": "error", "error": "no keys should be played"})
return
to_play = next(
(i for i in keys_to_play if i.key == key.key and i.done is not True), None
)
if to_play is None:
self.score -= 50
logging.info("Invalid key.")
if to_play:
perf = "practice"
logging.debug({"note_on": f"{perf} on {message.note}"})
self.send({"type": "timing", "id": message.id, "timing": perf})
else:
timingScore, _ = self.getTiming(key, to_play)
self.score += (
100
if timingScore == "perfect"
else 75
if timingScore == "great"
else 50
)
self.wrong_ids += [message.id]
logging.debug({"note_on": f"wrong key {message.note}"})
self.send({"type": "timing", "id": message.id, "timing": "wrong"})
def handleNoteOffPractice(self, message: NoteOffMessage):
logging.debug({"note_off": message.note})
down_since = next(
since for (h_key, since) in self.keys_down if h_key == message.note
)
self.keys_down.remove((message.note, down_since))
if message.id in self.wrong_ids:
logging.debug({"note_off": f"wrong key {message.note}"})
self.send({"type": "duration", "id": message.id, "duration": "wrong"})
return
key = Key(
key=message.note, start=down_since, duration=(message.time - down_since)
)
keys_to_play = next(
(i for i in self.practice_partition if any(x.done is not True for x in i)),
None,
)
if keys_to_play is None:
logging.info("Invalid key.")
self.info["score"] -= 50
# TODO: I dont think this if is right
# self.sendScore(message.id, "wrong key", "wrong key")
return
to_play = next(
(i for i in keys_to_play if i.key == key.key and i.done is not True), None
)
if to_play:
perf = "practice"
to_play.done = True
self.sendScore(message.id, timingScore, "practice")
logging.debug({"note_off": f"{perf} on {message.note}"})
self.send({"type": "duration", "id": message.id, "duration": perf})
else:
self.send({"type": "duration", "id": message.id, "duration": "wrong"})
def getTiming(self, key: Key, to_play: Key):
return self.getTimingScore(key, to_play), self.getTimingInfo(key, to_play)
def getTimingScore(self, key: Key, to_play: Key):
def getDurationScore(self, key: Key, to_play: Key):
tempo_percent = abs((key.duration / to_play.duration) - 1)
if tempo_percent < 0.3:
timingScore = "perfect"
elif tempo_percent < 0.5:
timingScore = "great"
timingScore = "short" if key.duration < to_play.duration else "long"
else:
timingScore = "good"
timingScore = "too short" if key.duration < to_play.duration else "too long"
return timingScore
def getTimingInfo(self, key: Key, to_play: Key):
def getTimingScore(self, key: Key, to_play: Key):
return (
"perfect"
if abs(key.start - to_play.start) < 200
else "fast"
if key.start < to_play.start
else "late"
if abs(key.start - to_play.start) < 100
else "great"
if (key.start < to_play.start) < 300
else "good"
)
# is it in the 500 ms range
@@ -268,7 +261,7 @@ class Scorometer:
match message:
case InvalidMessage(error):
logging.warning(f"Invalid message {line} with error: {error}")
send({"error": f"Invalid message {line} with error: {error}"})
self.send({"error": f"Invalid message {line} with error: {error}"})
case NoteOnMessage():
if self.mode == NORMAL:
self.handleNoteOn(message)
@@ -288,43 +281,25 @@ class Scorometer:
f"Expected note_on note_off or pause message but got {message.type} instead"
)
def sendScore(self, id, timingScore, timingInformation):
send(
{
"id": id,
"timingScore": timingScore,
"timingInformation": timingInformation,
}
)
def gameLoop(self):
while True:
if select.select(
[
sys.stdin,
],
[],
[],
0.0,
)[0]:
message, line = getMessage()
logging.info(f"handling message {line}")
self.handleMessage(message, line)
else:
pass
message, line = getMessage()
logging.debug(f"handling message {line}")
self.handleMessage(message, line)
def endGame(self):
for i in self.partition.notes:
if i.done is False:
self.score -= 50
self.info["score"] -= 50
self.info["missed"] += 1
send(
{
"overallScore": self.score,
"overallScore": self.info["score"],
"score": {
"missed": self.missed,
"good": self.good,
"great": self.great,
"perfect": self.perfect,
"missed": self.info["missed"],
"good": self.info["good"],
"great": self.info["great"],
"perfect": self.info["perfect"],
"maxScore": len(self.partition.notes) * 100,
},
}
@@ -335,7 +310,7 @@ class Scorometer:
json={
"songID": self.song_id,
"userID": self.user_id,
"score": self.score,
"score": self.info["score"],
"difficulties": self.difficulties,
},
)
@@ -399,4 +374,4 @@ def main():
if __name__ == "__main__":
main()
main()

View File

@@ -1,10 +1,19 @@
{"id": 2, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 3, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 4, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 5, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 6, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 7, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 8, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 9, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 10, "timingScore": "perfect", "timingInformation": "perfect"}
{"overallScore": 850, "score": {"missed": 0, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}}
{"type": "timing", "id": 2, "timing": "perfect", "info": {"max_score": 1000, "score": 0, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 2, "duration": "perfect", "info": {"max_score": 1000, "score": 100, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 3, "timing": "perfect", "info": {"max_score": 1000, "score": 100, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 3, "duration": "perfect", "info": {"max_score": 1000, "score": 200, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 4, "timing": "perfect", "info": {"max_score": 1000, "score": 200, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 4, "duration": "perfect", "info": {"max_score": 1000, "score": 300, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 5, "timing": "perfect", "info": {"max_score": 1000, "score": 300, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 5, "duration": "perfect", "info": {"max_score": 1000, "score": 400, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 6, "timing": "perfect", "info": {"max_score": 1000, "score": 400, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 6, "duration": "perfect", "info": {"max_score": 1000, "score": 500, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 7, "timing": "perfect", "info": {"max_score": 1000, "score": 500, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 7, "duration": "perfect", "info": {"max_score": 1000, "score": 600, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 8, "timing": "perfect", "info": {"max_score": 1000, "score": 600, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 8, "duration": "perfect", "info": {"max_score": 1000, "score": 700, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 9, "timing": "perfect", "info": {"max_score": 1000, "score": 700, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 9, "duration": "perfect", "info": {"max_score": 1000, "score": 800, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 10, "timing": "perfect", "info": {"max_score": 1000, "score": 800, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 10, "duration": "perfect", "info": {"max_score": 1000, "score": 900, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"overallScore": 850, "score": {"missed": 1, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}}

View File

@@ -1,11 +1,21 @@
{"id": 1, "timingScore": "perfect", "timingInformation": "fast"}
{"id": 2, "timingScore": "perfect", "timingInformation": "fast"}
{"id": 3, "timingScore": "perfect", "timingInformation": "fast"}
{"id": 4, "timingScore": "perfect", "timingInformation": "fast"}
{"id": 5, "timingScore": "perfect", "timingInformation": "fast"}
{"id": 6, "timingScore": "perfect", "timingInformation": "fast"}
{"id": 7, "timingScore": "perfect", "timingInformation": "fast"}
{"id": 8, "timingScore": "perfect", "timingInformation": "fast"}
{"id": 9, "timingScore": "perfect", "timingInformation": "fast"}
{"id": 10, "timingScore": "perfect", "timingInformation": "fast"}
{"type": "timing", "id": 1, "timing": "great", "info": {"max_score": 1000, "score": 0, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 1, "duration": "perfect", "info": {"max_score": 1000, "score": 100, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 2, "timing": "great", "info": {"max_score": 1000, "score": 100, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 2, "duration": "perfect", "info": {"max_score": 1000, "score": 200, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 3, "timing": "great", "info": {"max_score": 1000, "score": 200, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 3, "duration": "perfect", "info": {"max_score": 1000, "score": 300, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 4, "timing": "great", "info": {"max_score": 1000, "score": 300, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 4, "duration": "perfect", "info": {"max_score": 1000, "score": 400, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 5, "timing": "great", "info": {"max_score": 1000, "score": 400, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 5, "duration": "perfect", "info": {"max_score": 1000, "score": 500, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 6, "timing": "great", "info": {"max_score": 1000, "score": 500, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 6, "duration": "perfect", "info": {"max_score": 1000, "score": 600, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 7, "timing": "great", "info": {"max_score": 1000, "score": 600, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 7, "duration": "perfect", "info": {"max_score": 1000, "score": 700, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 8, "timing": "great", "info": {"max_score": 1000, "score": 700, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 8, "duration": "perfect", "info": {"max_score": 1000, "score": 800, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 9, "timing": "great", "info": {"max_score": 1000, "score": 800, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 9, "duration": "perfect", "info": {"max_score": 1000, "score": 900, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 10, "timing": "great", "info": {"max_score": 1000, "score": 900, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 10, "duration": "perfect", "info": {"max_score": 1000, "score": 1000, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"overallScore": 1000, "score": {"missed": 0, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}}

View File

@@ -1,9 +1,17 @@
{"id": 1, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 2, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 3, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 4, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 5, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 6, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 7, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 8, "timingScore": "perfect", "timingInformation": "perfect"}
{"overallScore": 700, "score": {"missed": 0, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}}
{"type": "timing", "id": 1, "timing": "perfect", "info": {"max_score": 1000, "score": 0, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 1, "duration": "perfect", "info": {"max_score": 1000, "score": 100, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 2, "timing": "perfect", "info": {"max_score": 1000, "score": 100, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 2, "duration": "perfect", "info": {"max_score": 1000, "score": 200, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 3, "timing": "perfect", "info": {"max_score": 1000, "score": 200, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 3, "duration": "perfect", "info": {"max_score": 1000, "score": 300, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 4, "timing": "perfect", "info": {"max_score": 1000, "score": 300, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 4, "duration": "perfect", "info": {"max_score": 1000, "score": 400, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 5, "timing": "perfect", "info": {"max_score": 1000, "score": 400, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 5, "duration": "perfect", "info": {"max_score": 1000, "score": 500, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 6, "timing": "perfect", "info": {"max_score": 1000, "score": 500, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 6, "duration": "perfect", "info": {"max_score": 1000, "score": 600, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 7, "timing": "perfect", "info": {"max_score": 1000, "score": 600, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 7, "duration": "perfect", "info": {"max_score": 1000, "score": 700, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 8, "timing": "perfect", "info": {"max_score": 1000, "score": 700, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 8, "duration": "perfect", "info": {"max_score": 1000, "score": 800, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"overallScore": 700, "score": {"missed": 2, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}}

View File

@@ -1,11 +1,21 @@
{"id": 1, "timingScore": "good", "timingInformation": "perfect"}
{"id": 2, "timingScore": "good", "timingInformation": "perfect"}
{"id": 3, "timingScore": "good", "timingInformation": "perfect"}
{"id": 4, "timingScore": "good", "timingInformation": "perfect"}
{"id": 5, "timingScore": "good", "timingInformation": "perfect"}
{"id": 6, "timingScore": "good", "timingInformation": "perfect"}
{"id": 7, "timingScore": "good", "timingInformation": "perfect"}
{"id": 8, "timingScore": "good", "timingInformation": "perfect"}
{"id": 9, "timingScore": "good", "timingInformation": "perfect"}
{"id": 10, "timingScore": "good", "timingInformation": "perfect"}
{"type": "timing", "id": 1, "timing": "perfect", "info": {"max_score": 1000, "score": 0, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 1, "duration": "too short", "info": {"max_score": 1000, "score": 50, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 2, "timing": "perfect", "info": {"max_score": 1000, "score": 50, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 2, "duration": "too short", "info": {"max_score": 1000, "score": 100, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 3, "timing": "perfect", "info": {"max_score": 1000, "score": 100, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 3, "duration": "too short", "info": {"max_score": 1000, "score": 150, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 4, "timing": "perfect", "info": {"max_score": 1000, "score": 150, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 4, "duration": "too short", "info": {"max_score": 1000, "score": 200, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 5, "timing": "perfect", "info": {"max_score": 1000, "score": 200, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 5, "duration": "too short", "info": {"max_score": 1000, "score": 250, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 6, "timing": "perfect", "info": {"max_score": 1000, "score": 250, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 6, "duration": "too short", "info": {"max_score": 1000, "score": 300, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 7, "timing": "perfect", "info": {"max_score": 1000, "score": 300, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 7, "duration": "too short", "info": {"max_score": 1000, "score": 350, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 8, "timing": "perfect", "info": {"max_score": 1000, "score": 350, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 8, "duration": "too short", "info": {"max_score": 1000, "score": 400, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 9, "timing": "perfect", "info": {"max_score": 1000, "score": 400, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 9, "duration": "too short", "info": {"max_score": 1000, "score": 450, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 10, "timing": "perfect", "info": {"max_score": 1000, "score": 450, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 10, "duration": "too short", "info": {"max_score": 1000, "score": 500, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"overallScore": 500, "score": {"missed": 0, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}}

View File

@@ -1,11 +1,21 @@
{"id": 1, "timingScore": "good", "timingInformation": "perfect"}
{"id": 2, "timingScore": "good", "timingInformation": "perfect"}
{"id": 3, "timingScore": "good", "timingInformation": "perfect"}
{"id": 4, "timingScore": "good", "timingInformation": "perfect"}
{"id": 5, "timingScore": "good", "timingInformation": "perfect"}
{"id": 6, "timingScore": "good", "timingInformation": "perfect"}
{"id": 7, "timingScore": "good", "timingInformation": "perfect"}
{"id": 8, "timingScore": "good", "timingInformation": "perfect"}
{"id": 9, "timingScore": "good", "timingInformation": "perfect"}
{"id": 10, "timingScore": "good", "timingInformation": "perfect"}
{"type": "timing", "id": 1, "timing": "perfect", "info": {"max_score": 1000, "score": 0, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 1, "duration": "too long", "info": {"max_score": 1000, "score": 50, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 2, "timing": "perfect", "info": {"max_score": 1000, "score": 50, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 2, "duration": "too long", "info": {"max_score": 1000, "score": 100, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 3, "timing": "perfect", "info": {"max_score": 1000, "score": 100, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 3, "duration": "too long", "info": {"max_score": 1000, "score": 150, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 4, "timing": "perfect", "info": {"max_score": 1000, "score": 150, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 4, "duration": "too long", "info": {"max_score": 1000, "score": 200, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 5, "timing": "perfect", "info": {"max_score": 1000, "score": 200, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 5, "duration": "too long", "info": {"max_score": 1000, "score": 250, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 6, "timing": "perfect", "info": {"max_score": 1000, "score": 250, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 6, "duration": "too long", "info": {"max_score": 1000, "score": 300, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 7, "timing": "perfect", "info": {"max_score": 1000, "score": 300, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 7, "duration": "too long", "info": {"max_score": 1000, "score": 350, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 8, "timing": "perfect", "info": {"max_score": 1000, "score": 350, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 8, "duration": "too long", "info": {"max_score": 1000, "score": 400, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 9, "timing": "perfect", "info": {"max_score": 1000, "score": 400, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 9, "duration": "too long", "info": {"max_score": 1000, "score": 450, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 10, "timing": "perfect", "info": {"max_score": 1000, "score": 450, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 10, "duration": "too long", "info": {"max_score": 1000, "score": 500, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"overallScore": 500, "score": {"missed": 0, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}}

View File

@@ -1,11 +1,21 @@
{"id": 1, "timingScore": "perfect", "timingInformation": "late"}
{"id": 2, "timingScore": "perfect", "timingInformation": "late"}
{"id": 3, "timingScore": "perfect", "timingInformation": "late"}
{"id": 4, "timingScore": "perfect", "timingInformation": "late"}
{"id": 5, "timingScore": "perfect", "timingInformation": "late"}
{"id": 6, "timingScore": "perfect", "timingInformation": "late"}
{"id": 7, "timingScore": "perfect", "timingInformation": "late"}
{"id": 8, "timingScore": "perfect", "timingInformation": "late"}
{"id": 9, "timingScore": "perfect", "timingInformation": "late"}
{"id": 10, "timingScore": "perfect", "timingInformation": "late"}
{"type": "timing", "id": 1, "timing": "great", "info": {"max_score": 1000, "score": 0, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 1, "duration": "perfect", "info": {"max_score": 1000, "score": 100, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 2, "timing": "great", "info": {"max_score": 1000, "score": 100, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 2, "duration": "perfect", "info": {"max_score": 1000, "score": 200, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 3, "timing": "great", "info": {"max_score": 1000, "score": 200, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 3, "duration": "perfect", "info": {"max_score": 1000, "score": 300, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 4, "timing": "great", "info": {"max_score": 1000, "score": 300, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 4, "duration": "perfect", "info": {"max_score": 1000, "score": 400, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 5, "timing": "great", "info": {"max_score": 1000, "score": 400, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 5, "duration": "perfect", "info": {"max_score": 1000, "score": 500, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 6, "timing": "great", "info": {"max_score": 1000, "score": 500, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 6, "duration": "perfect", "info": {"max_score": 1000, "score": 600, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 7, "timing": "great", "info": {"max_score": 1000, "score": 600, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 7, "duration": "perfect", "info": {"max_score": 1000, "score": 700, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 8, "timing": "great", "info": {"max_score": 1000, "score": 700, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 8, "duration": "perfect", "info": {"max_score": 1000, "score": 800, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 9, "timing": "great", "info": {"max_score": 1000, "score": 800, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 9, "duration": "perfect", "info": {"max_score": 1000, "score": 900, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 10, "timing": "great", "info": {"max_score": 1000, "score": 900, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 10, "duration": "perfect", "info": {"max_score": 1000, "score": 1000, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"overallScore": 1000, "score": {"missed": 0, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}}

View File

@@ -1,11 +1,21 @@
{"id": 1, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 2, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 3, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 4, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 5, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 6, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 7, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 8, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 9, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 10, "timingScore": "perfect", "timingInformation": "perfect"}
{"type": "timing", "id": 1, "timing": "perfect", "info": {"max_score": 1000, "score": 0, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 1, "duration": "perfect", "info": {"max_score": 1000, "score": 100, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 2, "timing": "perfect", "info": {"max_score": 1000, "score": 100, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 2, "duration": "perfect", "info": {"max_score": 1000, "score": 200, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 3, "timing": "perfect", "info": {"max_score": 1000, "score": 200, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 3, "duration": "perfect", "info": {"max_score": 1000, "score": 300, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 4, "timing": "perfect", "info": {"max_score": 1000, "score": 300, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 4, "duration": "perfect", "info": {"max_score": 1000, "score": 400, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 5, "timing": "perfect", "info": {"max_score": 1000, "score": 400, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 5, "duration": "perfect", "info": {"max_score": 1000, "score": 500, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 6, "timing": "perfect", "info": {"max_score": 1000, "score": 500, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 6, "duration": "perfect", "info": {"max_score": 1000, "score": 600, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 7, "timing": "perfect", "info": {"max_score": 1000, "score": 600, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 7, "duration": "perfect", "info": {"max_score": 1000, "score": 700, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 8, "timing": "perfect", "info": {"max_score": 1000, "score": 700, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 8, "duration": "perfect", "info": {"max_score": 1000, "score": 800, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 9, "timing": "perfect", "info": {"max_score": 1000, "score": 800, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 9, "duration": "perfect", "info": {"max_score": 1000, "score": 900, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 10, "timing": "perfect", "info": {"max_score": 1000, "score": 900, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 10, "duration": "perfect", "info": {"max_score": 1000, "score": 1000, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"overallScore": 1000, "score": {"missed": 0, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}}

View File

@@ -1,9 +1,17 @@
{"id": 1, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 3, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 4, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 5, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 6, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 8, "timingScore": "good", "timingInformation": "late"}
{"id": 9, "timingScore": "perfect", "timingInformation": "perfect"}
{"id": 10, "timingScore": "perfect", "timingInformation": "perfect"}
{"overallScore": 650, "score": {"missed": 0, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}}
{"type": "timing", "id": 1, "timing": "perfect", "info": {"max_score": 1000, "score": 0, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 1, "duration": "perfect", "info": {"max_score": 1000, "score": 100, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 3, "timing": "perfect", "info": {"max_score": 1000, "score": 100, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 3, "duration": "perfect", "info": {"max_score": 1000, "score": 200, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 4, "timing": "perfect", "info": {"max_score": 1000, "score": 200, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 4, "duration": "perfect", "info": {"max_score": 1000, "score": 300, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 5, "timing": "perfect", "info": {"max_score": 1000, "score": 300, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 5, "duration": "perfect", "info": {"max_score": 1000, "score": 400, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 6, "timing": "perfect", "info": {"max_score": 1000, "score": 400, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 6, "duration": "perfect", "info": {"max_score": 1000, "score": 500, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 8, "timing": "great", "info": {"max_score": 1000, "score": 500, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 8, "duration": "too long", "info": {"max_score": 1000, "score": 550, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 9, "timing": "perfect", "info": {"max_score": 1000, "score": 550, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 9, "duration": "perfect", "info": {"max_score": 1000, "score": 650, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "timing", "id": 10, "timing": "perfect", "info": {"max_score": 1000, "score": 650, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"type": "duration", "id": 10, "duration": "perfect", "info": {"max_score": 1000, "score": 750, "missed": 0, "perfect": 0, "great": 0, "good": 0}}
{"overallScore": 650, "score": {"missed": 2, "good": 0, "great": 0, "perfect": 0, "maxScore": 1000}}

View File

@@ -13,7 +13,13 @@ function test {
cat $1/input | BACK_URL="http://localhost:3000" MUSICS_FOLDER="../../musics/" python3 ../main.py 1> /tmp/scorometer_res 2> /tmp/scorometer_log
TESTS_DONE=$((TESTS_DONE + 1))
if ! diff $1/output /tmp/scorometer_res &>/dev/null; then
echo "$t failed, do runner.sh $t for more info"
echo "=========== CURRENT OUTPUT ==========="
cat /tmp/scorometer_res
echo "======================================"
echo "=========== EXPECTED OUTPUT =========="
cat $1/output
echo "======================================"
TESTS_FAILED=$((TESTS_FAILED + 1))
else
TESTS_SUCCESS=$((TESTS_SUCCESS + 1))