first commit
parent
2279e8376f
commit
2ffaafc425
|
|
@ -0,0 +1,2 @@
|
||||||
|
/mvnw text eol=lf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
HELP.md
|
||||||
|
target/
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
wrapperVersion=3.3.2
|
||||||
|
distributionType=only-script
|
||||||
|
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>eric</groupId>
|
||||||
|
<artifactId>Roullette</artifactId>
|
||||||
|
<version>0</version>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>${maven.compiler.source}</source>
|
||||||
|
<target>${maven.compiler.target}</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.0.0-M7</version>
|
||||||
|
<configuration>
|
||||||
|
<failIfNoTests>true</failIfNoTests>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<filters>
|
||||||
|
<filter>
|
||||||
|
<artifact>*:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/*.SF</exclude>
|
||||||
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
</filters>
|
||||||
|
<transformers>
|
||||||
|
<transformer>
|
||||||
|
<mainClass>eric.Roullette.App</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>5.9.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>junit-jupiter-params</artifactId>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.javalin</groupId>
|
||||||
|
<artifactId>javalin-testtools</artifactId>
|
||||||
|
<version>6.7.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>okhttp</artifactId>
|
||||||
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<junit.jupiter.version>5.9.2</junit.jupiter.version>
|
||||||
|
<jackson.version>2.16.0</jackson.version>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<javalin.version>6.7.0</javalin.version>
|
||||||
|
</properties>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,259 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Apache Maven Wrapper startup batch script, version 3.3.2
|
||||||
|
#
|
||||||
|
# Optional ENV vars
|
||||||
|
# -----------------
|
||||||
|
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||||
|
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||||
|
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||||
|
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
set -euf
|
||||||
|
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||||
|
|
||||||
|
# OS specific support.
|
||||||
|
native_path() { printf %s\\n "$1"; }
|
||||||
|
case "$(uname)" in
|
||||||
|
CYGWIN* | MINGW*)
|
||||||
|
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||||
|
native_path() { cygpath --path --windows "$1"; }
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# set JAVACMD and JAVACCMD
|
||||||
|
set_java_home() {
|
||||||
|
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||||
|
if [ -n "${JAVA_HOME-}" ]; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||||
|
|
||||||
|
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||||
|
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||||
|
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="$(
|
||||||
|
'set' +e
|
||||||
|
'unset' -f command 2>/dev/null
|
||||||
|
'command' -v java
|
||||||
|
)" || :
|
||||||
|
JAVACCMD="$(
|
||||||
|
'set' +e
|
||||||
|
'unset' -f command 2>/dev/null
|
||||||
|
'command' -v javac
|
||||||
|
)" || :
|
||||||
|
|
||||||
|
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||||
|
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# hash string like Java String::hashCode
|
||||||
|
hash_string() {
|
||||||
|
str="${1:-}" h=0
|
||||||
|
while [ -n "$str" ]; do
|
||||||
|
char="${str%"${str#?}"}"
|
||||||
|
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||||
|
str="${str#?}"
|
||||||
|
done
|
||||||
|
printf %x\\n $h
|
||||||
|
}
|
||||||
|
|
||||||
|
verbose() { :; }
|
||||||
|
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf %s\\n "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
trim() {
|
||||||
|
# MWRAPPER-139:
|
||||||
|
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||||
|
# Needed for removing poorly interpreted newline sequences when running in more
|
||||||
|
# exotic environments such as mingw bash on Windows.
|
||||||
|
printf "%s" "${1}" | tr -d '[:space:]'
|
||||||
|
}
|
||||||
|
|
||||||
|
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||||
|
while IFS="=" read -r key value; do
|
||||||
|
case "${key-}" in
|
||||||
|
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||||
|
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||||
|
esac
|
||||||
|
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
|
||||||
|
case "${distributionUrl##*/}" in
|
||||||
|
maven-mvnd-*bin.*)
|
||||||
|
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||||
|
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||||
|
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||||
|
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||||
|
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||||
|
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||||
|
*)
|
||||||
|
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||||
|
distributionPlatform=linux-amd64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||||
|
;;
|
||||||
|
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||||
|
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||||
|
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||||
|
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||||
|
distributionUrlName="${distributionUrl##*/}"
|
||||||
|
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||||
|
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||||
|
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||||
|
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||||
|
|
||||||
|
exec_maven() {
|
||||||
|
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||||
|
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -d "$MAVEN_HOME" ]; then
|
||||||
|
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||||
|
exec_maven "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${distributionUrl-}" in
|
||||||
|
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||||
|
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# prepare tmp dir
|
||||||
|
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||||
|
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||||
|
trap clean HUP INT TERM EXIT
|
||||||
|
else
|
||||||
|
die "cannot create temp dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||||
|
|
||||||
|
# Download and Install Apache Maven
|
||||||
|
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||||
|
verbose "Downloading from: $distributionUrl"
|
||||||
|
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
|
||||||
|
# select .zip or .tar.gz
|
||||||
|
if ! command -v unzip >/dev/null; then
|
||||||
|
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||||
|
distributionUrlName="${distributionUrl##*/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# verbose opt
|
||||||
|
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||||
|
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||||
|
|
||||||
|
# normalize http auth
|
||||||
|
case "${MVNW_PASSWORD:+has-password}" in
|
||||||
|
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||||
|
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||||
|
verbose "Found wget ... using wget"
|
||||||
|
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||||
|
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||||
|
verbose "Found curl ... using curl"
|
||||||
|
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||||
|
elif set_java_home; then
|
||||||
|
verbose "Falling back to use Java to download"
|
||||||
|
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||||
|
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
cat >"$javaSource" <<-END
|
||||||
|
public class Downloader extends java.net.Authenticator
|
||||||
|
{
|
||||||
|
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||||
|
{
|
||||||
|
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||||
|
}
|
||||||
|
public static void main( String[] args ) throws Exception
|
||||||
|
{
|
||||||
|
setDefault( new Downloader() );
|
||||||
|
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
END
|
||||||
|
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||||
|
verbose " - Compiling Downloader.java ..."
|
||||||
|
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||||
|
verbose " - Running Downloader.java ..."
|
||||||
|
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||||
|
if [ -n "${distributionSha256Sum-}" ]; then
|
||||||
|
distributionSha256Result=false
|
||||||
|
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||||
|
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||||
|
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||||
|
exit 1
|
||||||
|
elif command -v sha256sum >/dev/null; then
|
||||||
|
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
|
||||||
|
distributionSha256Result=true
|
||||||
|
fi
|
||||||
|
elif command -v shasum >/dev/null; then
|
||||||
|
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||||
|
distributionSha256Result=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||||
|
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ $distributionSha256Result = false ]; then
|
||||||
|
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||||
|
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# unzip and move
|
||||||
|
if command -v unzip >/dev/null; then
|
||||||
|
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||||
|
else
|
||||||
|
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||||
|
fi
|
||||||
|
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
|
||||||
|
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||||
|
|
||||||
|
clean || :
|
||||||
|
exec_maven "$@"
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
<# : batch portion
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
@REM or more contributor license agreements. See the NOTICE file
|
||||||
|
@REM distributed with this work for additional information
|
||||||
|
@REM regarding copyright ownership. The ASF licenses this file
|
||||||
|
@REM to you under the Apache License, Version 2.0 (the
|
||||||
|
@REM "License"); you may not use this file except in compliance
|
||||||
|
@REM with the License. You may obtain a copy of the License at
|
||||||
|
@REM
|
||||||
|
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@REM
|
||||||
|
@REM Unless required by applicable law or agreed to in writing,
|
||||||
|
@REM software distributed under the License is distributed on an
|
||||||
|
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
@REM KIND, either express or implied. See the License for the
|
||||||
|
@REM specific language governing permissions and limitations
|
||||||
|
@REM under the License.
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
@REM Apache Maven Wrapper startup batch script, version 3.3.2
|
||||||
|
@REM
|
||||||
|
@REM Optional ENV vars
|
||||||
|
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||||
|
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||||
|
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||||
|
@SET __MVNW_CMD__=
|
||||||
|
@SET __MVNW_ERROR__=
|
||||||
|
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||||
|
@SET PSModulePath=
|
||||||
|
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||||
|
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||||
|
)
|
||||||
|
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||||
|
@SET __MVNW_PSMODULEP_SAVE=
|
||||||
|
@SET __MVNW_ARG0_NAME__=
|
||||||
|
@SET MVNW_USERNAME=
|
||||||
|
@SET MVNW_PASSWORD=
|
||||||
|
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
|
||||||
|
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||||
|
@GOTO :EOF
|
||||||
|
: end batch / begin powershell #>
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
if ($env:MVNW_VERBOSE -eq "true") {
|
||||||
|
$VerbosePreference = "Continue"
|
||||||
|
}
|
||||||
|
|
||||||
|
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||||
|
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||||
|
if (!$distributionUrl) {
|
||||||
|
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||||
|
"maven-mvnd-*" {
|
||||||
|
$USE_MVND = $true
|
||||||
|
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||||
|
$MVN_CMD = "mvnd.cmd"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
$USE_MVND = $false
|
||||||
|
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||||
|
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||||
|
if ($env:MVNW_REPOURL) {
|
||||||
|
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||||
|
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
|
||||||
|
}
|
||||||
|
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||||
|
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||||
|
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
|
||||||
|
if ($env:MAVEN_USER_HOME) {
|
||||||
|
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
|
||||||
|
}
|
||||||
|
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||||
|
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||||
|
|
||||||
|
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||||
|
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||||
|
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||||
|
exit $?
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||||
|
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||||
|
}
|
||||||
|
|
||||||
|
# prepare tmp dir
|
||||||
|
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||||
|
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||||
|
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||||
|
trap {
|
||||||
|
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||||
|
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||||
|
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||||
|
|
||||||
|
# Download and Install Apache Maven
|
||||||
|
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||||
|
Write-Verbose "Downloading from: $distributionUrl"
|
||||||
|
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
|
||||||
|
$webclient = New-Object System.Net.WebClient
|
||||||
|
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||||
|
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||||
|
}
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||||
|
|
||||||
|
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||||
|
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||||
|
if ($distributionSha256Sum) {
|
||||||
|
if ($USE_MVND) {
|
||||||
|
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||||
|
}
|
||||||
|
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||||
|
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||||
|
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# unzip and move
|
||||||
|
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||||
|
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||||
|
try {
|
||||||
|
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||||
|
} catch {
|
||||||
|
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||||
|
Write-Error "fail to move MAVEN_HOME"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||||
|
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||||
|
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>eric</groupId>
|
||||||
|
<artifactId>Roullette</artifactId>
|
||||||
|
<version>0</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<!-- Java -->
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|
||||||
|
<!-- Versions -->
|
||||||
|
<javalin.version>6.7.0</javalin.version>
|
||||||
|
<junit.jupiter.version>5.9.2</junit.jupiter.version>
|
||||||
|
<jackson.version>2.16.0</jackson.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Spotify API -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>se.michaelthelin.spotify</groupId>
|
||||||
|
<artifactId>spotify-web-api-java</artifactId>
|
||||||
|
<version>9.3.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Javalin -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.javalin</groupId>
|
||||||
|
<artifactId>javalin-bundle</artifactId>
|
||||||
|
<version>${javalin.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Jackson for application -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-core</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-annotations</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Test dependencies -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>${junit.jupiter.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.javalin</groupId>
|
||||||
|
<artifactId>javalin-testtools</artifactId>
|
||||||
|
<version>${javalin.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<!-- Compile plugin -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>${maven.compiler.source}</source>
|
||||||
|
<target>${maven.compiler.target}</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Surefire for running tests -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.0.0-M7</version>
|
||||||
|
<configuration>
|
||||||
|
<failIfNoTests>true</failIfNoTests>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Shade plugin for fat-jar -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<filters>
|
||||||
|
<filter>
|
||||||
|
<artifact>*:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/*.SF</exclude>
|
||||||
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
</filters>
|
||||||
|
<transformers>
|
||||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>eric.Roullette.App</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
package eric.Roullette;
|
||||||
|
|
||||||
|
import eric.Roullette.config.AppConfig;
|
||||||
|
import eric.Roullette.controller.GameController;
|
||||||
|
import eric.Roullette.service.GameService;
|
||||||
|
import eric.Roullette.service.SpotifyAuthService;
|
||||||
|
import eric.Roullette.websocket.GameWebSocketHandler;
|
||||||
|
import io.javalin.Javalin;
|
||||||
|
import io.javalin.http.staticfiles.Location;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class App {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(App.class);
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
AppConfig cfg = new AppConfig();
|
||||||
|
if (cfg.spotifyClientId == null || cfg.spotifyClientSecret == null || cfg.spotifyRedirectUri == null) {
|
||||||
|
throw new IllegalStateException("Spotify-Konfiguration fehlt: Bitte stelle sicher, dass ClientId, ClientSecret und RedirectUri gesetzt sind.");
|
||||||
|
}
|
||||||
|
GameService gs = new GameService();
|
||||||
|
SpotifyAuthService sas = new SpotifyAuthService(
|
||||||
|
cfg.spotifyClientId,
|
||||||
|
cfg.spotifyClientSecret,
|
||||||
|
cfg.spotifyRedirectUri
|
||||||
|
);
|
||||||
|
|
||||||
|
Javalin app = Javalin.create(config -> {
|
||||||
|
config.showJavalinBanner = false;
|
||||||
|
config.staticFiles.add("/public", Location.CLASSPATH);
|
||||||
|
}).start(cfg.port);
|
||||||
|
|
||||||
|
app.exception(Exception.class, (e, ctx) -> {
|
||||||
|
log.error("Unhandled error", e);
|
||||||
|
ctx.status(500).json(Map.of("error", e.getMessage()));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/", ctx -> ctx.redirect("/index.html"));
|
||||||
|
|
||||||
|
// OAuth
|
||||||
|
app.get("/login", ctx -> ctx.redirect(sas.getAuthorizationUri(ctx.queryParam("username")).toString()));
|
||||||
|
app.get("/spotify/callback", ctx -> {
|
||||||
|
String code = ctx.queryParam("code");
|
||||||
|
String user = ctx.queryParam("state"); // Benutzername aus dem 'state' Parameter holen
|
||||||
|
try {
|
||||||
|
sas.exchangeCode(code, user); // Benutzername an den Service übergeben
|
||||||
|
ctx.redirect("/lobby.html?username=" + URLEncoder.encode(user, UTF_8));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Fehler beim Austausch des Spotify-Codes für Benutzer {}", user, e);
|
||||||
|
ctx.status(500).result("Fehler bei der Spotify-Authentifizierung.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// WS-Handler
|
||||||
|
GameWebSocketHandler wsHandler = new GameWebSocketHandler(gs, sas);
|
||||||
|
|
||||||
|
// HTTP-Controller
|
||||||
|
new GameController(app, gs, sas, wsHandler);
|
||||||
|
|
||||||
|
app.ws("/ws/{gameId}", wsHandler::register);
|
||||||
|
|
||||||
|
log.info("Server läuft auf Port {}", cfg.port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package eric.Roullette;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class User {
|
||||||
|
private String name;
|
||||||
|
private List<String> recentTracks;
|
||||||
|
|
||||||
|
public User(String name, List<String> recentTracks) {
|
||||||
|
this.name = name;
|
||||||
|
this.recentTracks = recentTracks;
|
||||||
|
}
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
public List<String> getRecentTracks() {
|
||||||
|
return recentTracks;
|
||||||
|
}
|
||||||
|
public void setRecentTracks(List<String> recentTracks) {
|
||||||
|
this.recentTracks = recentTracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "User{" +
|
||||||
|
"name='" + name + '\'' +
|
||||||
|
", recentTracks=" + recentTracks +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package eric.Roullette.config;
|
||||||
|
|
||||||
|
public class AppConfig {
|
||||||
|
public final int port = Integer.parseInt(
|
||||||
|
System.getenv().getOrDefault("PORT", "8080")
|
||||||
|
);
|
||||||
|
public final String spotifyClientId = System.getenv("SPOTIFY_CLIENT_ID");
|
||||||
|
public final String spotifyClientSecret = System.getenv("SPOTIFY_CLIENT_SECRET");
|
||||||
|
public final String spotifyRedirectUri = System.getenv("SPOTIFY_REDIRECT_URI");
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
package eric.Roullette.controller;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import io.javalin.Javalin;
|
||||||
|
import io.javalin.http.Context;
|
||||||
|
import eric.Roullette.service.GameService;
|
||||||
|
import eric.Roullette.service.SpotifyAuthService;
|
||||||
|
import eric.Roullette.websocket.GameWebSocketHandler;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
public class GameController {
|
||||||
|
private final GameService gameService;
|
||||||
|
private final SpotifyAuthService authService;
|
||||||
|
private final GameWebSocketHandler webSocketHandler;
|
||||||
|
|
||||||
|
public GameController(Javalin app, GameService gs, SpotifyAuthService sas, GameWebSocketHandler wsHandler) {
|
||||||
|
this.gameService = gs;
|
||||||
|
this.authService = sas;
|
||||||
|
this.webSocketHandler = wsHandler;
|
||||||
|
app.post("/api/create-game", this::createGame);
|
||||||
|
app.post("/api/join-game", this::joinGame);
|
||||||
|
app.get("/api/game/{gameId}/players", this::getPlayers);
|
||||||
|
app.post("/api/game/{gameId}/start-round", this::startRound);
|
||||||
|
app.post("/api/game/{gameId}/guess", this::guess);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createGame(Context ctx) {
|
||||||
|
Map<String, Object> body = ctx.bodyAsClass(Map.class);
|
||||||
|
String user = (String) body.get("username");
|
||||||
|
if (user == null || user.isBlank()) {
|
||||||
|
ctx.status(400).result("username fehlt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int limit = body.get("limit") != null ? (int) body.get("limit") : 10;
|
||||||
|
|
||||||
|
String gameId;
|
||||||
|
do {
|
||||||
|
gameId = String.format("%04d", ThreadLocalRandom.current().nextInt(0, 10000));
|
||||||
|
} while (gameService.gameExists(gameId));
|
||||||
|
gameService.createGame(gameId, limit);
|
||||||
|
gameService.addPlayer(gameId, user, limit);
|
||||||
|
gameService.broadcastPlayers(gameId);
|
||||||
|
|
||||||
|
ctx.json(Map.of("status", "ok", "gameId", gameId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void joinGame(Context ctx) {
|
||||||
|
Map<String, String> body = ctx.bodyAsClass(Map.class);
|
||||||
|
String user = body.get("username");
|
||||||
|
String gameId = body.get("gameId");
|
||||||
|
int limit = body.get("limit") != null ? Integer.parseInt(body.get("limit")) : 10;
|
||||||
|
if (user == null || gameId == null) {
|
||||||
|
ctx.status(400).result("username oder gameId fehlt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gameService.addPlayer(gameId, user, limit);
|
||||||
|
gameService.broadcastPlayers(gameId);
|
||||||
|
ctx.json(Map.of("status", "ok"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getPlayers(Context ctx) {
|
||||||
|
String gameId = ctx.pathParam("gameId");
|
||||||
|
int limit = Integer.parseInt(Objects.requireNonNull(ctx.queryParam("limit")));
|
||||||
|
var game = gameService.getOrCreateGame(gameId, limit);
|
||||||
|
ctx.json(Map.of(
|
||||||
|
"players", game.players(),
|
||||||
|
"limit", game.limit() // Limit mitgeben!
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startRound(Context ctx) {
|
||||||
|
String gameId = ctx.pathParam("gameId");
|
||||||
|
int limit = Integer.parseInt(Objects.requireNonNull(ctx.queryParam("limit")));
|
||||||
|
ctx.json(Map.of("status", "ok"));
|
||||||
|
webSocketHandler.broadcastRoundStart(gameId, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void guess(Context ctx) {
|
||||||
|
String gameId = ctx.pathParam("gameId");
|
||||||
|
int limit = Integer.parseInt(Objects.requireNonNull(ctx.queryParam("limit")));
|
||||||
|
Map<String, String> body = ctx.bodyAsClass(Map.class);
|
||||||
|
String guess = body.get("guess");
|
||||||
|
String user = body.get("username");
|
||||||
|
GameService.Game game = gameService.getOrCreateGame(gameId, limit);
|
||||||
|
String owner = game.currentOwner();
|
||||||
|
if (owner == null || guess == null) {
|
||||||
|
ctx.status(400).result("ungültig");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean correct = guess.equals(owner);
|
||||||
|
if (correct) game.scores().merge(user, 1, Integer::sum);
|
||||||
|
ctx.json(Map.of(
|
||||||
|
"correct", correct,
|
||||||
|
"owner", owner,
|
||||||
|
"scores", game.scores()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package eric.Roullette.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird von Jackson kleinbuchstabig serialisiert,
|
||||||
|
* damit frontend und backend denselben `type` haben.
|
||||||
|
*/
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
public enum MessageType {
|
||||||
|
@JsonProperty("players") PLAYERS,
|
||||||
|
@JsonProperty("reload") RELOAD
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package eric.Roullette.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
public record PlayersMessage(
|
||||||
|
MessageType type,
|
||||||
|
List<String> players
|
||||||
|
) { public PlayersMessage(List<String> players) { this(MessageType.PLAYERS, players); } }
|
||||||
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package eric.Roullette.dto;
|
||||||
|
|
||||||
|
public record ReloadMessage(MessageType type) {
|
||||||
|
public ReloadMessage() { this(MessageType.RELOAD); }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package eric.Roullette.service;
|
||||||
|
|
||||||
|
import eric.Roullette.dto.PlayersMessage;
|
||||||
|
import eric.Roullette.util.JsonUtil;
|
||||||
|
import io.javalin.websocket.WsContext;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
public class GameService {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GameService.class);
|
||||||
|
private final Map<String, Set<WsContext>> sessions = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Game> games = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public record Game(String id, List<String> players, Map<String,Integer> scores,String currentOwner,
|
||||||
|
String currentSong,List<String> allTracks, int limit) {
|
||||||
|
public static Game create(String id, int limit) {
|
||||||
|
return new Game(id, new CopyOnWriteArrayList<>(), new ConcurrentHashMap<>(), null, null, new ArrayList<>(), limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public Game getOrCreateGame(String gameId, int limit) {
|
||||||
|
return games.computeIfAbsent(gameId, id -> Game.create(id, limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addPlayer(String gameId, String user, int limit) {
|
||||||
|
Game g = getOrCreateGame(gameId, limit);
|
||||||
|
if (user != null && !g.players().contains(user)) {
|
||||||
|
g.players().add(user);
|
||||||
|
g.scores().putIfAbsent(user, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerSession(String gameId, WsContext ctx) {
|
||||||
|
sessions
|
||||||
|
.computeIfAbsent(gameId, id -> ConcurrentHashMap.newKeySet())
|
||||||
|
.add(ctx);
|
||||||
|
broadcastPlayers(gameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeSession(String gameId, WsContext ctx) {
|
||||||
|
sessions.getOrDefault(gameId, Collections.emptySet()).remove(ctx);
|
||||||
|
broadcastPlayers(gameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void broadcastPlayers(String gameId) {
|
||||||
|
Game game = games.get(gameId);
|
||||||
|
if (game == null) return;
|
||||||
|
PlayersMessage msg = new PlayersMessage(new ArrayList<>(game.players()));
|
||||||
|
sessions.getOrDefault(gameId, Collections.emptySet())
|
||||||
|
.forEach(ctx -> ctx.send(JsonUtil.toJson(msg)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createGame(String gameId, int limit) {
|
||||||
|
Game game = new Game(gameId, new CopyOnWriteArrayList<>(), new ConcurrentHashMap<>(), null, null, new ArrayList<>(), limit);
|
||||||
|
games.put(gameId, game);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean gameExists(String gameId) {
|
||||||
|
return games.containsKey(gameId);
|
||||||
|
}
|
||||||
|
public Game startRound(String gameId, List<String> uris, int limit) {
|
||||||
|
Game g = getOrCreateGame(gameId, limit);
|
||||||
|
if (g.players().isEmpty()) throw new IllegalStateException("No players");
|
||||||
|
String owner = g.players().get(ThreadLocalRandom.current().nextInt(g.players().size()));
|
||||||
|
String song = uris.get(ThreadLocalRandom.current().nextInt(uris.size()));
|
||||||
|
Game updated = new Game(gameId, g.players(), g.scores(), owner, song, uris, limit);
|
||||||
|
games.put(gameId, updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
// In GameService.java
|
||||||
|
public Set<WsContext> getSessions(String gameId) {
|
||||||
|
return sessions.getOrDefault(gameId, Collections.emptySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
package eric.Roullette.service;
|
||||||
|
|
||||||
|
import org.apache.hc.core5.http.ParseException;
|
||||||
|
import se.michaelthelin.spotify.SpotifyApi;
|
||||||
|
import se.michaelthelin.spotify.SpotifyHttpManager;
|
||||||
|
import se.michaelthelin.spotify.exceptions.SpotifyWebApiException;
|
||||||
|
import se.michaelthelin.spotify.model_objects.credentials.AuthorizationCodeCredentials;
|
||||||
|
import se.michaelthelin.spotify.model_objects.specification.Paging;
|
||||||
|
import se.michaelthelin.spotify.model_objects.specification.PagingCursorbased;
|
||||||
|
import se.michaelthelin.spotify.model_objects.specification.PlayHistory;
|
||||||
|
import se.michaelthelin.spotify.requests.data.player.GetCurrentUsersRecentlyPlayedTracksRequest;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class SpotifyAuthService {
|
||||||
|
private final String clientId;
|
||||||
|
private final String clientSecret;
|
||||||
|
private final URI redirectUri;
|
||||||
|
// Speichert für jeden Benutzer eine eigene, authentifizierte SpotifyApi-Instanz
|
||||||
|
private final Map<String, SpotifyApi> userApis = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public SpotifyAuthService(String clientId, String clientSecret, String redirectUri) {
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.clientSecret = clientSecret;
|
||||||
|
this.redirectUri = SpotifyHttpManager.makeUri(redirectUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public URI getAuthorizationUri(String user) {
|
||||||
|
// Temporäre API-Instanz nur für die Erstellung der Auth-URL
|
||||||
|
SpotifyApi tempApi = new SpotifyApi.Builder()
|
||||||
|
.setClientId(clientId)
|
||||||
|
.setClientSecret(clientSecret)
|
||||||
|
.setRedirectUri(redirectUri)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return tempApi.authorizationCodeUri()
|
||||||
|
.scope("user-read-recently-played")
|
||||||
|
.state(user) // Der Benutzername wird im State mitgegeben
|
||||||
|
.build()
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void exchangeCode(String code, String user) throws IOException, ParseException, SpotifyWebApiException {
|
||||||
|
// Erstellt eine neue, dedizierte API-Instanz für diesen Benutzer
|
||||||
|
SpotifyApi userApi = new SpotifyApi.Builder()
|
||||||
|
.setClientId(clientId)
|
||||||
|
.setClientSecret(clientSecret)
|
||||||
|
.setRedirectUri(redirectUri)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Tauscht den Code gegen Tokens und konfiguriert die Instanz
|
||||||
|
AuthorizationCodeCredentials creds = userApi.authorizationCode(code).build().execute();
|
||||||
|
userApi.setAccessToken(creds.getAccessToken());
|
||||||
|
userApi.setRefreshToken(creds.getRefreshToken());
|
||||||
|
|
||||||
|
// Speichert die fertig konfigurierte API-Instanz für den Benutzer
|
||||||
|
userApis.put(user, userApi);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getRecentTracks(String user, int limit) {
|
||||||
|
SpotifyApi userApi = userApis.get(user);
|
||||||
|
|
||||||
|
if (userApi == null) {
|
||||||
|
System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + user);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
GetCurrentUsersRecentlyPlayedTracksRequest request = userApi.getCurrentUsersRecentlyPlayedTracks()
|
||||||
|
.limit(limit)
|
||||||
|
.build();
|
||||||
|
PagingCursorbased<PlayHistory> history = request.execute();
|
||||||
|
if (history == null || history.getItems() == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return Arrays.stream(history.getItems())
|
||||||
|
.map(item -> item.getTrack().getUri())
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
} catch (IOException | SpotifyWebApiException | ParseException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getRecentTracksLimit(SpotifyApi userApi, int limit) throws IOException, SpotifyWebApiException, ParseException {
|
||||||
|
GetCurrentUsersRecentlyPlayedTracksRequest request = userApi.getCurrentUsersRecentlyPlayedTracks()
|
||||||
|
.limit(limit)
|
||||||
|
.build();
|
||||||
|
PagingCursorbased<PlayHistory> history = request.execute();
|
||||||
|
|
||||||
|
List<String> uris = Arrays.stream(history.getItems())
|
||||||
|
.map(item -> item.getTrack().getUri())
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
if (uris.size() < limit) {
|
||||||
|
int newLimit = limit + (limit - uris.size());
|
||||||
|
return getRecentTracksLimit(userApi, newLimit);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package eric.Roullette.util;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility für JSON-Serialisierung/Deserialisierung via Jackson.
|
||||||
|
*/
|
||||||
|
public final class JsonUtil {
|
||||||
|
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper()
|
||||||
|
// Optional: schönere Ausgabe, falls Du Debug-JSON brauchst
|
||||||
|
.configure(SerializationFeature.INDENT_OUTPUT, false);
|
||||||
|
|
||||||
|
private JsonUtil() { /* no instances */ }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialisiert ein Objekt in einen JSON-String.
|
||||||
|
*/
|
||||||
|
public static String toJson(Object obj) {
|
||||||
|
try {
|
||||||
|
return MAPPER.writeValueAsString(obj);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("JSON serialization failed for " + obj, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialisiert einen JSON-String in eine Instanz von {@code clazz}.
|
||||||
|
*/
|
||||||
|
public static <T> T fromJson(String json, Class<T> clazz) {
|
||||||
|
try {
|
||||||
|
return MAPPER.readValue(json, clazz);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("JSON deserialization failed for " + clazz, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
package eric.Roullette.websocket;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import eric.Roullette.service.GameService;
|
||||||
|
import eric.Roullette.service.SpotifyAuthService;
|
||||||
|
import eric.Roullette.util.JsonUtil;
|
||||||
|
import io.javalin.websocket.WsConfig;
|
||||||
|
import io.javalin.websocket.WsContext;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket-Handler für Spiel-Sessions. Verwaltet Spieler-Sessions,
|
||||||
|
* broadcastet Spieler-Listen, Runden-Starts und Rundenergebnisse.
|
||||||
|
*/
|
||||||
|
public class GameWebSocketHandler {
|
||||||
|
|
||||||
|
private final GameService service;
|
||||||
|
private final SpotifyAuthService authService;
|
||||||
|
|
||||||
|
// Spiel-ID → (Username → deren Guess)
|
||||||
|
private final Map<String, Map<String, String>> currentGuesses = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public GameWebSocketHandler(GameService gameService, SpotifyAuthService authService) {
|
||||||
|
this.service = gameService;
|
||||||
|
this.authService = authService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registriert Connect/Close/Message-Handler für eine WebSocket-Route.
|
||||||
|
*/
|
||||||
|
public void register(WsConfig ws) {
|
||||||
|
|
||||||
|
// Neue Connection
|
||||||
|
ws.onConnect(ctx -> {
|
||||||
|
String gameId = ctx.pathParam("gameId");
|
||||||
|
String username = ctx.queryParam("username");
|
||||||
|
int limit = Integer.parseInt(Objects.requireNonNull(ctx.queryParam("limit")));
|
||||||
|
// Spiel- und Session-Registrierung
|
||||||
|
service.addPlayer(gameId, username, limit);
|
||||||
|
// Alle Clients über neue Spielerliste informieren
|
||||||
|
service.registerSession(gameId, ctx);
|
||||||
|
service.broadcastPlayers(gameId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connection geschlossen
|
||||||
|
ws.onClose(ctx -> {
|
||||||
|
String gameId = ctx.pathParam("gameId");
|
||||||
|
service.removeSession(gameId, ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Eingehende Nachrichten (Guesses & Player-Requests)
|
||||||
|
ws.onMessage(ctx -> {
|
||||||
|
String gameId = ctx.pathParam("gameId");
|
||||||
|
int limit = Integer.parseInt(Objects.requireNonNull(ctx.queryParam("limit")));
|
||||||
|
JsonNode node = JsonUtil.fromJson(ctx.message(), JsonNode.class);
|
||||||
|
String type = node.get("type").asText();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "guess" -> {
|
||||||
|
String user = node.get("username").asText();
|
||||||
|
String guess = node.get("guess").asText();
|
||||||
|
// Guess speichern
|
||||||
|
currentGuesses
|
||||||
|
.computeIfAbsent(gameId, id -> new ConcurrentHashMap<>())
|
||||||
|
.put(user, guess);
|
||||||
|
// Wenn alle getippt haben, Ergebnis broadcasten
|
||||||
|
int numPlayers = service.getOrCreateGame(gameId, limit).players().size();
|
||||||
|
if (currentGuesses.get(gameId).size() == numPlayers) {
|
||||||
|
broadcastRoundResult(gameId, limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "requestPlayers" -> service.broadcastPlayers(gameId);
|
||||||
|
|
||||||
|
case "start-round" -> {
|
||||||
|
var game = service.getOrCreateGame(gameId,limit);
|
||||||
|
if (game.players().isEmpty()) return;
|
||||||
|
|
||||||
|
// Songs von allen Spielern sammeln
|
||||||
|
List<String> allTracks = new ArrayList<>();
|
||||||
|
for (String player : game.players()) {
|
||||||
|
allTracks.addAll(authService.getRecentTracks(player, game.limit()));
|
||||||
|
}
|
||||||
|
if (allTracks.isEmpty()) {
|
||||||
|
// TODO: Fehler an Client senden, dass keine Songs da sind
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runde im Service starten, um Song und Owner zu setzen
|
||||||
|
service.startRound(gameId, allTracks,limit);
|
||||||
|
// Jetzt Broadcast mit den aktuellen Daten
|
||||||
|
broadcastRoundStart(gameId, limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Broadcast-Methoden -----
|
||||||
|
|
||||||
|
/** Broadcastet den Runden-Start (Song + Optionen) an alle Clients. */
|
||||||
|
public void broadcastRoundStart(String gameId, int limit) {
|
||||||
|
var game = service.getOrCreateGame(gameId,limit);
|
||||||
|
List<String> opts = game.players();
|
||||||
|
String songUri = game.currentSong();
|
||||||
|
List<String> allTracks = game.allTracks();
|
||||||
|
String msg = JsonUtil.toJson(Map.of(
|
||||||
|
"type", "round-start",
|
||||||
|
"ownerOptions", opts,
|
||||||
|
"songUri", songUri,
|
||||||
|
"allTracks", allTracks
|
||||||
|
));
|
||||||
|
broadcastToAll(gameId, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broadcastet das Rundenergebnis (Scores, wer richtig, wer getippt hat). */
|
||||||
|
// Punkte für alle Guess-Teilnehmer anpassen
|
||||||
|
private void broadcastRoundResult(String gameId, int limit) {
|
||||||
|
var game = service.getOrCreateGame(gameId, limit);
|
||||||
|
Map<String,Integer> scores = game.scores();
|
||||||
|
Map<String,String> guesses = currentGuesses.remove(gameId);
|
||||||
|
String owner = game.currentOwner();
|
||||||
|
|
||||||
|
// Für jeden Tippenden Score anpassen
|
||||||
|
for (Map.Entry<String, String> entry : guesses.entrySet()) {
|
||||||
|
String guesser = entry.getKey();
|
||||||
|
boolean correct = owner.equals(entry.getValue());
|
||||||
|
scores.merge(guesser, correct ? 3 : -1, Integer::sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
String msg = JsonUtil.toJson(Map.of(
|
||||||
|
"type", "round-result",
|
||||||
|
"scores", scores,
|
||||||
|
"guesses", guesses,
|
||||||
|
"owner", owner
|
||||||
|
));
|
||||||
|
broadcastToAll(gameId, msg);
|
||||||
|
|
||||||
|
// Prüfe auf Gewinner
|
||||||
|
String winner = scores.entrySet().stream()
|
||||||
|
.filter(e -> e.getValue() >= 30)
|
||||||
|
.map(Map.Entry::getKey)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
if (winner != null) {
|
||||||
|
// Broadcast an alle, dass das Spiel vorbei ist
|
||||||
|
String winMsg = JsonUtil.toJson(Map.of(
|
||||||
|
"type", "game-end",
|
||||||
|
"winner", winner,
|
||||||
|
"scores", scores
|
||||||
|
));
|
||||||
|
broadcastToAll(gameId, winMsg);
|
||||||
|
game.scores().replaceAll((user , pts) -> 0); // Reset Scores für alle Spieler
|
||||||
|
|
||||||
|
}else{
|
||||||
|
// nächste Runde starten
|
||||||
|
new Thread(() -> {
|
||||||
|
try { Thread.sleep(4000); } catch (InterruptedException ignored) {}
|
||||||
|
broadcastRoundStart(gameId, limit);
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hilfsmethode: Sendet eine Nachricht an alle WebSocket-Sessions eines Spiels. */
|
||||||
|
private void broadcastToAll(String gameId, String msg) {
|
||||||
|
// Holt alle WsContext, die der Service beim Connect registriert hat
|
||||||
|
Set<WsContext> sessions = service.getSessions(gameId);
|
||||||
|
if (sessions == null) return;
|
||||||
|
sessions.stream()
|
||||||
|
.filter(s -> s.session.isOpen())
|
||||||
|
.forEach(ctx -> {
|
||||||
|
try {
|
||||||
|
ctx.send(msg);
|
||||||
|
} catch (Exception ignore) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
spring.application.name=Roullette
|
||||||
|
|
||||||
|
spotify:
|
||||||
|
client-id: 70c36cd6e2d54ad0ba2e60ef6334bbc8
|
||||||
|
client-secret: 116188574dd140eab1973e75c7e5ecfe
|
||||||
|
redirect-uri: https://www.davidmagkuchen.de/spotify/callback
|
||||||
|
|
@ -0,0 +1,280 @@
|
||||||
|
package eric.Roullette;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.javalin.Javalin;
|
||||||
|
import io.javalin.http.staticfiles.Location;
|
||||||
|
import io.javalin.websocket.WsContext;
|
||||||
|
import se.michaelthelin.spotify.SpotifyApi;
|
||||||
|
import se.michaelthelin.spotify.SpotifyHttpManager;
|
||||||
|
import se.michaelthelin.spotify.model_objects.credentials.AuthorizationCodeCredentials;
|
||||||
|
import se.michaelthelin.spotify.requests.authorization.authorization_code.AuthorizationCodeUriRequest;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
public class App {
|
||||||
|
|
||||||
|
// JSON-Mapper
|
||||||
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
// WebSocket-Sessions per gameId
|
||||||
|
private static final ConcurrentMap<String, Set<WsContext>> gameSockets = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// In-memory stores
|
||||||
|
private static final ConcurrentMap<String, Game> games = new ConcurrentHashMap<>();
|
||||||
|
private static final ConcurrentMap<String, List<String>> userRecentTracks = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Spotify API credentials
|
||||||
|
private static final String clientId = "70c36cd6e2d54ad0ba2e60ef6334bbc8";
|
||||||
|
private static final String clientSecret = "116188574dd140eab1973e75c7e5ecfe";
|
||||||
|
private static final URI redirectUri = SpotifyHttpManager.makeUri(
|
||||||
|
"https://www.davidmagkuchen.de/spotify/callback"
|
||||||
|
);
|
||||||
|
private static final SpotifyApi spotifyApi = new SpotifyApi.Builder()
|
||||||
|
.setClientId(clientId)
|
||||||
|
.setClientSecret(clientSecret)
|
||||||
|
.setRedirectUri(redirectUri)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Game record
|
||||||
|
record Game(
|
||||||
|
String id,
|
||||||
|
List<String> players,
|
||||||
|
Map<String,Integer> scores,
|
||||||
|
String currentOwner,
|
||||||
|
String currentSong
|
||||||
|
) {
|
||||||
|
static Game create(String id) {
|
||||||
|
return new Game(id,
|
||||||
|
new ArrayList<>(),
|
||||||
|
new ConcurrentHashMap<>(),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
int limit = 20;
|
||||||
|
|
||||||
|
// Create Javalin
|
||||||
|
Javalin app = Javalin.create(config -> {
|
||||||
|
config.staticFiles.add(sf -> {
|
||||||
|
sf.directory = "/public";
|
||||||
|
sf.location = Location.CLASSPATH;
|
||||||
|
sf.hostedPath = "/";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// WebSocket-Endpoint
|
||||||
|
app.ws("/ws/{gameId}", ws -> {
|
||||||
|
ws.onConnect(ctx -> {
|
||||||
|
String gameId = ctx.pathParam("gameId");
|
||||||
|
String username = ctx.queryParam("username");
|
||||||
|
|
||||||
|
// Session registrieren
|
||||||
|
Set<WsContext> sessions = gameSockets
|
||||||
|
.computeIfAbsent(gameId, id -> ConcurrentHashMap.newKeySet());
|
||||||
|
sessions.add(ctx);
|
||||||
|
|
||||||
|
// Game anlegen + Spieler hinzufügen
|
||||||
|
Game game = games.computeIfAbsent(gameId, Game::create);
|
||||||
|
if (username != null
|
||||||
|
&& !username.isBlank()
|
||||||
|
&& game.players().add(username)) {
|
||||||
|
game.scores().putIfAbsent(username, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Immer an alle pushen
|
||||||
|
broadcastPlayers(gameId);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.onClose(ctx -> {
|
||||||
|
String gameId = ctx.pathParam("gameId");
|
||||||
|
gameSockets
|
||||||
|
.getOrDefault(gameId, Collections.emptySet())
|
||||||
|
.remove(ctx);
|
||||||
|
broadcastPlayers(gameId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.start(8080);
|
||||||
|
|
||||||
|
// Redirect root to index
|
||||||
|
app.get("/", ctx -> ctx.redirect("/index.html"));
|
||||||
|
|
||||||
|
// OAuth login
|
||||||
|
app.get("/login", ctx -> {
|
||||||
|
String user = ctx.queryParam("username");
|
||||||
|
if (user == null || user.isEmpty()) {
|
||||||
|
ctx.status(400).result("username fehlt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AuthorizationCodeUriRequest req = spotifyApi.authorizationCodeUri()
|
||||||
|
.scope("user-read-recently-played")
|
||||||
|
.state(user)
|
||||||
|
.build();
|
||||||
|
ctx.redirect(req.execute().toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
// OAuth callback
|
||||||
|
app.get("/spotify/callback", ctx -> {
|
||||||
|
String code = ctx.queryParam("code");
|
||||||
|
String user = ctx.queryParam("state");
|
||||||
|
if (code == null || user == null) {
|
||||||
|
ctx.status(400).result("code oder state fehlt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AuthorizationCodeCredentials creds = spotifyApi
|
||||||
|
.authorizationCode(code).build().execute();
|
||||||
|
spotifyApi.setAccessToken(creds.getAccessToken());
|
||||||
|
spotifyApi.setRefreshToken(creds.getRefreshToken());
|
||||||
|
|
||||||
|
var history = spotifyApi.getCurrentUsersRecentlyPlayedTracks()
|
||||||
|
.limit(limit).build().execute().getItems();
|
||||||
|
List<String> uris = Arrays.stream(history)
|
||||||
|
.map(it -> it.getTrack().getUri())
|
||||||
|
.toList();
|
||||||
|
userRecentTracks.put(user, uris);
|
||||||
|
|
||||||
|
String uenc = URLEncoder.encode(user, StandardCharsets.UTF_8);
|
||||||
|
ctx.redirect("/lobby.html?username=" + uenc);
|
||||||
|
});
|
||||||
|
|
||||||
|
// create game
|
||||||
|
app.post("/api/create-game", ctx -> {
|
||||||
|
Map<String,Object> body = ctx.bodyAsClass(Map.class);
|
||||||
|
String user = (String) body.get("username");
|
||||||
|
if (user == null || user.isEmpty()) {
|
||||||
|
ctx.status(400).result("username fehlt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String gameId;
|
||||||
|
do {
|
||||||
|
gameId = String.format("%04d", ThreadLocalRandom.current().nextInt(10000));
|
||||||
|
} while (games.containsKey(gameId));
|
||||||
|
Game game = Game.create(gameId);
|
||||||
|
game.players().add(user);
|
||||||
|
game.scores().put(user, 0);
|
||||||
|
games.put(gameId, game);
|
||||||
|
broadcastPlayers(gameId);
|
||||||
|
broadcastReload(gameId);
|
||||||
|
ctx.json(Map.of("status","ok","gameId",gameId));
|
||||||
|
});
|
||||||
|
|
||||||
|
// join game
|
||||||
|
app.post("/api/join-game", ctx -> {
|
||||||
|
Map<String,Object> body = ctx.bodyAsClass(Map.class);
|
||||||
|
String user = (String) body.get("username");
|
||||||
|
String gameId = (String) body.get("gameId");
|
||||||
|
if (user == null || gameId == null) {
|
||||||
|
ctx.status(400).result("username oder gameId fehlt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Game game = games.get(gameId);
|
||||||
|
if (game == null) {
|
||||||
|
ctx.status(404).result("Game nicht gefunden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (game.players().add(user)) {
|
||||||
|
game.scores().putIfAbsent(user, 0);
|
||||||
|
}
|
||||||
|
broadcastPlayers(gameId);
|
||||||
|
broadcastReload(gameId);
|
||||||
|
ctx.json(Map.of("status","ok"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// start round
|
||||||
|
app.post("/api/game/{gameId}/start-round", ctx -> {
|
||||||
|
String gameId = ctx.pathParam("gameId");
|
||||||
|
Game game = games.get(gameId);
|
||||||
|
if (game == null || game.players().isEmpty()) {
|
||||||
|
ctx.status(400).result("Game ungültig");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> pls = new ArrayList<>(game.players());
|
||||||
|
String owner = pls.get(ThreadLocalRandom.current().nextInt(pls.size()));
|
||||||
|
List<String> uris = userRecentTracks.get(owner);
|
||||||
|
if (uris == null || uris.isEmpty()) {
|
||||||
|
ctx.status(400).result("keine Tracks");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String song = uris.get(ThreadLocalRandom.current().nextInt(uris.size()));
|
||||||
|
game = new Game(game.id(), game.players(), game.scores(), owner, song);
|
||||||
|
games.put(gameId, game);
|
||||||
|
ctx.json(Map.of(
|
||||||
|
"ownerOptions", game.players(),
|
||||||
|
"songUri", song,
|
||||||
|
"scores", game.scores()
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
// guess
|
||||||
|
app.post("/api/game/{gameId}/guess", ctx -> {
|
||||||
|
String gameId = ctx.pathParam("gameId");
|
||||||
|
Map<String,Object> body = ctx.bodyAsClass(Map.class);
|
||||||
|
String guess = (String) body.get("guess");
|
||||||
|
String user = (String) body.get("username");
|
||||||
|
Game game = games.get(gameId);
|
||||||
|
if (game == null || game.currentOwner() == null || guess == null) {
|
||||||
|
ctx.status(400).result("ungültig");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean correct = guess.equals(game.currentOwner());
|
||||||
|
if (correct) game.scores().merge(user, 1, Integer::sum);
|
||||||
|
ctx.json(Map.of(
|
||||||
|
"correct", correct,
|
||||||
|
"owner", game.currentOwner(),
|
||||||
|
"scores", game.scores()
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
// players list
|
||||||
|
app.get("/api/game/{gameId}/players", ctx -> {
|
||||||
|
Game game = games.get(ctx.pathParam("gameId"));
|
||||||
|
if (game == null) {
|
||||||
|
ctx.status(404).result("Game nicht gefunden");
|
||||||
|
} else {
|
||||||
|
ctx.json(Map.of("players", new ArrayList<>(game.players())));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// broadcast helper: sendet an alle Clients
|
||||||
|
private static void broadcastPlayers(String gameId) {
|
||||||
|
Game game = games.get(gameId);
|
||||||
|
Set<WsContext> sessions = gameSockets.get(gameId);
|
||||||
|
if (game == null || sessions == null) return;
|
||||||
|
Map<String,Object> payload = Map.of(
|
||||||
|
"type", "players",
|
||||||
|
"players", new ArrayList<>(game.players())
|
||||||
|
);
|
||||||
|
String msg;
|
||||||
|
try {
|
||||||
|
msg = objectMapper.writeValueAsString(payload);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sessions.forEach(ctx -> {
|
||||||
|
try { ctx.send(msg); } catch (Exception ignore) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// broadcast reload: schickt allen Clients ein Reload-Event
|
||||||
|
private static void broadcastReload(String gameId) {
|
||||||
|
Set<WsContext> sessions = gameSockets.get(gameId);
|
||||||
|
if (sessions == null) return;
|
||||||
|
sessions.forEach(ctx -> {
|
||||||
|
try {
|
||||||
|
ctx.send("{\"type\":\"reload\"}");
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head><meta charset="UTF-8"><title>Spiel erstellen</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Neues Spiel</h1>
|
||||||
|
<button id="createGame">Spiel erstellen</button>
|
||||||
|
<script>
|
||||||
|
// Username aus URL holen
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const username = params.get('username');
|
||||||
|
if (!username) {
|
||||||
|
alert('Kein Username gefunden!');
|
||||||
|
window.location.href = '/lobby.html';
|
||||||
|
}
|
||||||
|
document.getElementById("createGame").addEventListener("click", async () => {
|
||||||
|
const res = await fetch("/api/create-game", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({ username })
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.status === "ok") {
|
||||||
|
// Spiel-Code anzeigen oder in URL weiterleiten
|
||||||
|
alert("Dein Spiel-Code: " + json.gameId);
|
||||||
|
window.location.href = "/game.html?gameId=" + json.gameId + "&username=" + encodeURIComponent(username);
|
||||||
|
} else {
|
||||||
|
alert("Fehler beim Erstellen");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Spotify Roulette – Spiel</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; max-width: 600px; margin: auto; padding: 1rem; }
|
||||||
|
#options button { margin: 0.5rem; }
|
||||||
|
#scoreboard { margin-top: 1rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Spotify Roulette</h1>
|
||||||
|
<p>Spiel-Code: <strong id="gameId"></strong></p>
|
||||||
|
|
||||||
|
<h2>Teilnehmer</h2>
|
||||||
|
<ul id="playersList"></ul>
|
||||||
|
|
||||||
|
<div id="controls">
|
||||||
|
<button id="startRound">Runde starten</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="roundArea" hidden>
|
||||||
|
<h2>Wer hat’s gehört?</h2>
|
||||||
|
<div id="songEmbed"></div>
|
||||||
|
<div id="options"></div>
|
||||||
|
<p id="result"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Scoreboard</h2>
|
||||||
|
<ul id="scoreboard"></ul>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const gameId = params.get('gameId');
|
||||||
|
const username = params.get('username');
|
||||||
|
if (!gameId) {
|
||||||
|
alert('Ungültige oder fehlende gameId in URL!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('gameId').textContent = gameId;
|
||||||
|
|
||||||
|
// Teilnehmerliste initial laden
|
||||||
|
async function loadPlayers() {
|
||||||
|
const res = await fetch(`/api/game/${gameId}/players`);
|
||||||
|
if (res.ok) {
|
||||||
|
const { players = [] } = await res.json();
|
||||||
|
const ul = document.getElementById("playersList");
|
||||||
|
ul.innerHTML = "";
|
||||||
|
players.forEach(user => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.textContent = user;
|
||||||
|
if (user === username) li.style.fontWeight = 'bold';
|
||||||
|
ul.append(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadPlayers();
|
||||||
|
|
||||||
|
// WebSocket-Verbindung
|
||||||
|
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
||||||
|
const socket = new WebSocket(`${protocol}://${location.host}/ws/${gameId}?username=${encodeURIComponent(username)}`);
|
||||||
|
|
||||||
|
socket.onopen = () => {
|
||||||
|
// Liste frisch laden, falls das erste Broadcast verpasst wurde
|
||||||
|
loadPlayers();
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = event => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === "players") {
|
||||||
|
const ul = document.getElementById("playersList");
|
||||||
|
ul.innerHTML = "";
|
||||||
|
(data.players || []).forEach(user => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.textContent = user;
|
||||||
|
if (user === username) li.style.fontWeight = 'bold';
|
||||||
|
ul.append(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (data.type === "reload") {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startBtn = document.getElementById('startRound');
|
||||||
|
const roundArea = document.getElementById('roundArea');
|
||||||
|
const songEmbed = document.getElementById('songEmbed');
|
||||||
|
const optionsDiv = document.getElementById('options');
|
||||||
|
const resultP = document.getElementById('result');
|
||||||
|
const scoreboard = document.getElementById('scoreboard');
|
||||||
|
|
||||||
|
function renderScoreboard(scores) {
|
||||||
|
scoreboard.innerHTML = "";
|
||||||
|
for (const [user, pts] of Object.entries(scores)) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = `${user}: ${pts} Punkte`;
|
||||||
|
scoreboard.append(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startBtn.addEventListener('click', async () => {
|
||||||
|
resultP.textContent = "";
|
||||||
|
optionsDiv.innerHTML = "";
|
||||||
|
songEmbed.innerHTML = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/game/${gameId}/start-round`, { method: 'POST' });
|
||||||
|
if (!res.ok) {
|
||||||
|
alert(`Fehler beim Starten der Runde: ${await res.text()}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { ownerOptions, songUri, scores } = await res.json();
|
||||||
|
renderScoreboard(scores);
|
||||||
|
|
||||||
|
const trackId = songUri.split(':')[2];
|
||||||
|
songEmbed.innerHTML = `
|
||||||
|
<iframe
|
||||||
|
src="https://open.spotify.com/embed/track/${trackId}"
|
||||||
|
width="100%" height="80" frameborder="0"
|
||||||
|
allow="encrypted-media">
|
||||||
|
</iframe>`;
|
||||||
|
|
||||||
|
ownerOptions.forEach(user => {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.textContent = user;
|
||||||
|
btn.onclick = () => makeGuess(user);
|
||||||
|
optionsDiv.append(btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
startBtn.hidden = true;
|
||||||
|
roundArea.hidden = false;
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Netzwerkfehler: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function makeGuess(guess) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/game/${gameId}/guess`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type':'application/json'},
|
||||||
|
body: JSON.stringify({ guess, username })
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
alert(`Fehler beim Tippen: ${await res.text()}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { correct, owner, scores } = await res.json();
|
||||||
|
resultP.textContent = correct
|
||||||
|
? '✅ Richtig!'
|
||||||
|
: `❌ Falsch. Es war: ${owner}`;
|
||||||
|
renderScoreboard(scores);
|
||||||
|
startBtn.hidden = false;
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Netzwerkfehler: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Zuletzt gespielt</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Spotify – Zuletzt gehörte Songs</h1>
|
||||||
|
<form id="usernameForm">
|
||||||
|
<label for="username">Benutzername:</label>
|
||||||
|
<input type="text" id="username" name="username" required>
|
||||||
|
<button type="submit">Weiter mit Spotify</button>
|
||||||
|
</form>
|
||||||
|
<ul id="tracks"></ul>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// document, nicht document
|
||||||
|
document.getElementById('usernameForm').addEventListener('submit', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const u = document.getElementById('username').value;
|
||||||
|
// direkt zum OAuth-Login, username im state-Param
|
||||||
|
window.location = `/login?username=${encodeURIComponent(u)}`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head><meta charset="UTF-8"><title>Spiel beitreten</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Spiel beitreten</h1>
|
||||||
|
<form id="joinForm">
|
||||||
|
<input id="gameId" name="gameId" placeholder="4-stelliger Code" required pattern="\d{4}"/>
|
||||||
|
<button type="submit">Beitreten</button>
|
||||||
|
</form>
|
||||||
|
<script>
|
||||||
|
// Username aus URL holen
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const username = params.get('username');
|
||||||
|
document.getElementById("joinForm").addEventListener("submit", async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const id = document.getElementById("gameId").value;
|
||||||
|
if (!username) {
|
||||||
|
alert('Kein Username gefunden!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await fetch("/api/join-game", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({gameId: id, username})
|
||||||
|
});
|
||||||
|
if (res.ok) window.location.href = "/game.html?gameId=" + id + "&username=" + encodeURIComponent(username);
|
||||||
|
else alert("Spiel nicht gefunden");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<!-- This HTML file allows users to join an existing game by entering the game ID. -->
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Spotify Roulette – Lobby</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Lobby</h1>
|
||||||
|
<p>Was möchtest du tun?</p>
|
||||||
|
<button id="createGame">Spiel erstellen</button>
|
||||||
|
<button id="joinGame">Spiel beitreten</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Username aus URL holen
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const username = params.get('username');
|
||||||
|
document.getElementById("createGame").addEventListener("click", () => {
|
||||||
|
// Username an create-game.html anhängen
|
||||||
|
if (username) {
|
||||||
|
window.location.href = "/create-game.html?username=" + encodeURIComponent(username);
|
||||||
|
} else {
|
||||||
|
alert('Kein Username gefunden!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById("joinGame").addEventListener("click", () => {
|
||||||
|
if (username) {
|
||||||
|
window.location.href = "/join-game.html?username=" + encodeURIComponent(username);
|
||||||
|
} else {
|
||||||
|
window.location.href = "/join-game.html";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
1. Limit manuell einstellbar
|
||||||
|
2. songs die schon gespielt wurden nicht mehr spielen
|
||||||
|
3. songs die schon gespielt wurden nicht mehr in der Liste anzeigen
|
||||||
|
4. songs die schon gespielt wurden in einer extra Liste anzeigen
|
||||||
|
mehrere auswählbar, wenn man alle richtig hat einen extra punkt kriegen
|
||||||
|
verhindern dass 3 mal hinterander der gleicher owner bzw. das gleiche lied kommt
|
||||||
|
songs die schon gespielt wurden in extra liste anzeigen um doppelte songs zu vermeiden
|
||||||
|
(evtl. feste liste für spätere runden)
|
||||||
|
Schauen ob andere auch einen song in der liste haben für mehrere auswahlen
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Spiel erstellen – Spotify Roulette</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<label for="limit">Wie viele kürzlich gehörte Songs sollen verwendet werden?</label>
|
||||||
|
<select id="limit">
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
<option value="500">500</option>
|
||||||
|
</select>
|
||||||
|
<h1>Neues Spiel erstellen</h1>
|
||||||
|
<button id="createGame">Spiel erstellen</button>
|
||||||
|
|
||||||
|
<script type="module" src="/js/create-game.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Spotify Roulette – Spiel</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; max-width: 600px; margin: auto; padding: 1rem; }
|
||||||
|
#options button { margin: 0.5rem; }
|
||||||
|
#scoreboard { margin-top: 1rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Spotify Roulette</h1>
|
||||||
|
<p>Spiel-Code: <strong id="gameId"></strong></p>
|
||||||
|
|
||||||
|
<div id="songListArea" style="position:fixed; right:0; top:0; width:200px; height:100vh; overflow-y:auto; background:#f8f8f8; border-left:1px solid #ccc; padding:1rem; z-index:10;">
|
||||||
|
<h3>Geladene Songs</h3>
|
||||||
|
<ul id="songList"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Teilnehmer</h2>
|
||||||
|
<ul id="playersList"></ul>
|
||||||
|
|
||||||
|
<div id="controls">
|
||||||
|
<button id="startRound">Runde starten</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="roundArea" hidden>
|
||||||
|
<h2>Wer hat’s gehört?</h2>
|
||||||
|
<div id="songEmbed"></div>
|
||||||
|
<div id="options"></div>
|
||||||
|
<p id="result"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Scoreboard</h2>
|
||||||
|
<ul id="scoreboard"></ul>
|
||||||
|
|
||||||
|
|
||||||
|
<script type="module" src="/js/game.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Spotify – Zuletzt gehörte Songs</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Spotify – Zuletzt gehörte Songs</h1>
|
||||||
|
<form id="usernameForm">
|
||||||
|
<label for="username">Benutzername:</label>
|
||||||
|
<input type="text" id="username" name="username" required>
|
||||||
|
<button type="submit">Weiter mit Spotify</button>
|
||||||
|
</form>
|
||||||
|
<ul id="tracks"></ul>
|
||||||
|
|
||||||
|
<script type="module" src="/js/login.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Spiel beitreten – Spotify Roulette</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Spiel beitreten</h1>
|
||||||
|
<form id="joinForm">
|
||||||
|
<input id="gameId" name="gameId" placeholder="4-stelliger Code" required pattern="\d{4}">
|
||||||
|
<button type="submit">Beitreten</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script type="module" src="/js/join-game.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// public/js/create.js
|
||||||
|
|
||||||
|
import { getParam, fetchJson } from "./utils.js";
|
||||||
|
|
||||||
|
const username = getParam("username");
|
||||||
|
if (!username) {
|
||||||
|
alert("Kein Username gefunden!");
|
||||||
|
window.location.href = "/lobby.html";
|
||||||
|
throw new Error("Missing username");
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("createGame").addEventListener("click", async () => {
|
||||||
|
const limit = parseInt(document.getElementById("limit").value, 10);
|
||||||
|
try {
|
||||||
|
const { status, gameId } = await fetchJson("/api/create-game", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ username, limit })
|
||||||
|
});
|
||||||
|
if (status === "ok") {
|
||||||
|
window.location.href = `/game.html?gameId=${gameId}&username=${encodeURIComponent(username)}&limit=${limit}`; } else {
|
||||||
|
alert("Fehler beim Erstellen des Spiels");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Fehler: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
// public/js/game.js
|
||||||
|
|
||||||
|
import { getParam, fetchJson, renderList } from "./utils.js";
|
||||||
|
import { setupStartRound } from "./start-round.js";
|
||||||
|
|
||||||
|
const gameId = getParam("gameId");
|
||||||
|
const username = getParam("username");
|
||||||
|
const limit = getParam("limit");
|
||||||
|
|
||||||
|
if (!limit) {
|
||||||
|
alert("Limit fehlt!");
|
||||||
|
throw new Error("Missing limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Parameter prüfen
|
||||||
|
if (!gameId || !username) {
|
||||||
|
alert("Ungültige oder fehlende URL-Parameter!");
|
||||||
|
throw new Error("Missing gameId or username");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Copy to clipboard (unverändert)
|
||||||
|
(function copyCodeToClipboard(code) {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
navigator.clipboard.writeText(code)
|
||||||
|
.then(() => console.log(`GameCode ${code} copied to clipboard`))
|
||||||
|
.catch(err => console.error("Clipboard write failed:", err));
|
||||||
|
} else {
|
||||||
|
const ta = document.createElement("textarea");
|
||||||
|
ta.value = code;
|
||||||
|
ta.style.position = "fixed";
|
||||||
|
ta.style.left = "-9999px";
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.focus();
|
||||||
|
ta.select();
|
||||||
|
try {
|
||||||
|
document.execCommand("copy");
|
||||||
|
console.log(`GameCode ${code} copied via execCommand`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Fallback copy failed:", err);
|
||||||
|
}
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
}
|
||||||
|
})(gameId);
|
||||||
|
|
||||||
|
// 3) Visuelles Feedback (unverändert)
|
||||||
|
const notice = document.createElement("div");
|
||||||
|
notice.textContent = `Spiel-Code ${gameId} in die Zwischenablage kopiert!`;
|
||||||
|
Object.assign(notice.style, { /* … Styles … */ });
|
||||||
|
document.body.append(notice);
|
||||||
|
setTimeout(() => notice.remove(), 3000);
|
||||||
|
|
||||||
|
// 4) Spiel-Code ins DOM schreiben
|
||||||
|
document.getElementById("gameId").textContent = gameId;
|
||||||
|
|
||||||
|
// 5) WebSocket einrichten und Spieler laden
|
||||||
|
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
||||||
|
const socket = new WebSocket(
|
||||||
|
`${protocol}://${location.host}/ws/${gameId}?username=${encodeURIComponent(username)}&limit=${limit}`
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.addEventListener("open", () => {
|
||||||
|
console.log("WebSocket connected. Requesting player list...");
|
||||||
|
// Fordere die Spielerliste vom Server an, sobald die Verbindung steht.
|
||||||
|
socket.send(JSON.stringify({ type: "requestPlayers" }));
|
||||||
|
// Binde den Handler für den "Runde starten"-Button
|
||||||
|
setupStartRound(socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6) WebSocket-Nachrichten verarbeiten
|
||||||
|
socket.addEventListener("message", ({ data }) => {
|
||||||
|
console.log("WS-Rohdaten:", data);
|
||||||
|
const msg = JSON.parse(data);
|
||||||
|
|
||||||
|
|
||||||
|
switch (msg.type) {
|
||||||
|
case "players":
|
||||||
|
console.log("Empfangene Spieler:", msg.players);
|
||||||
|
renderList("#playersList", msg.players, username);
|
||||||
|
break;
|
||||||
|
case "reload":
|
||||||
|
window.location.reload();
|
||||||
|
break;
|
||||||
|
case "round-start":
|
||||||
|
handleRoundStart(msg);
|
||||||
|
break;
|
||||||
|
case "round-result":
|
||||||
|
handleRoundResult(msg);
|
||||||
|
break;
|
||||||
|
case "game-end":
|
||||||
|
handleGameEnd(msg);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn("Unknown WS message type:", msg.type);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 8) Funktion zum Anzeigen einer neuen Runde
|
||||||
|
const startBtn = document.getElementById("startRound");
|
||||||
|
const roundArea = document.getElementById("roundArea");
|
||||||
|
const songEmbed = document.getElementById("songEmbed");
|
||||||
|
const optionsDiv = document.getElementById("options");
|
||||||
|
const resultP = document.getElementById("result");
|
||||||
|
const scoreboard = document.getElementById("scoreboard");
|
||||||
|
|
||||||
|
function handleRoundStart({ ownerOptions, songUri, allTracks }) {
|
||||||
|
// UI zurücksetzen
|
||||||
|
resultP.textContent = "";
|
||||||
|
optionsDiv.innerHTML = "";
|
||||||
|
songEmbed.innerHTML = "";
|
||||||
|
|
||||||
|
// Song einbetten
|
||||||
|
const trackId = songUri.split(":")[2];
|
||||||
|
songEmbed.innerHTML = `
|
||||||
|
<iframe
|
||||||
|
src="https://open.spotify.com/embed/track/${trackId}"
|
||||||
|
width="100%" height="80" frameborder="0"
|
||||||
|
allow="encrypted-media">
|
||||||
|
</iframe>`;
|
||||||
|
|
||||||
|
// Tipp-Buttons erzeugen
|
||||||
|
ownerOptions.forEach(user => {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.textContent = user;
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
type: "guess",
|
||||||
|
username: username,
|
||||||
|
guess: user
|
||||||
|
}));
|
||||||
|
// Nach Tipp alle Buttons deaktivieren
|
||||||
|
optionsDiv.querySelectorAll("button").forEach(b => b.disabled = true);
|
||||||
|
});
|
||||||
|
optionsDiv.append(btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
// UI anzeigen
|
||||||
|
startBtn.hidden = true;
|
||||||
|
roundArea.hidden = false;
|
||||||
|
const songList = document.getElementById("songList");
|
||||||
|
songList.innerHTML = "";
|
||||||
|
if (Array.isArray(allTracks)) {
|
||||||
|
allTracks.forEach(uri => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.textContent = uri;
|
||||||
|
songList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9) Funktion zum Anzeigen des Ergebnisses
|
||||||
|
function renderScoreboard(scores) {
|
||||||
|
scoreboard.innerHTML = "";
|
||||||
|
Object.entries(scores).forEach(([user, pts]) => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.textContent = `${user}: ${pts} Punkte`;
|
||||||
|
scoreboard.append(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function handleRoundResult({ scores, guesses, owner }) {
|
||||||
|
// Scoreboard updaten
|
||||||
|
renderScoreboard(scores);
|
||||||
|
|
||||||
|
// Ergebnis für alle Spieler anzeigen
|
||||||
|
resultP.innerHTML = ""; // Vorher leeren
|
||||||
|
Object.entries(guesses).forEach(([user, guess]) => {
|
||||||
|
const correct = guess === owner;
|
||||||
|
const icon = correct ? "✅" : "❌";
|
||||||
|
const msg = `${icon} ${user} hat auf ${guess} getippt${correct ? " (richtig!)" : " (falsch)"}`;
|
||||||
|
const p = document.createElement("p");
|
||||||
|
p.textContent = msg;
|
||||||
|
resultP.appendChild(p);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nach kurzer Pause für die nächste Runde vorbereiten
|
||||||
|
setTimeout(() => {
|
||||||
|
resultP.textContent = "";
|
||||||
|
startBtn.hidden = true;
|
||||||
|
startBtn.disabled = true;
|
||||||
|
roundArea.hidden = true;
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGameEnd({winner}) {
|
||||||
|
resultP.textContent = `🎉 ${winner} hat gewonnen!`;
|
||||||
|
startBtn.hidden = false;
|
||||||
|
roundArea.hidden = true;
|
||||||
|
startBtn.disabled = false;
|
||||||
|
// scoreboard leeren
|
||||||
|
scoreboard.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
// public/js/join.js
|
||||||
|
|
||||||
|
import { getParam, fetchJson } from "./utils.js";
|
||||||
|
|
||||||
|
const username = getParam("username");
|
||||||
|
|
||||||
|
document.getElementById("joinForm").addEventListener("submit", async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const gameId = document.getElementById("gameId").value;
|
||||||
|
if (!username) {
|
||||||
|
alert("Kein Username gefunden!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetchJson(`/api/game/${gameId}/players?limit=50`); // Dummy-Limit, Backend ignoriert es beim getOrCreateGame
|
||||||
|
const limit = res && res.players ? res.limit : 10; // Backend sollte das Limit mitliefern!
|
||||||
|
await fetchJson("/api/join-game", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ gameId, username })
|
||||||
|
});
|
||||||
|
window.location.href = `/game.html?gameId=${gameId}&username=${encodeURIComponent(username)}&limit=${limit}`;
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Fehler: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
// public/js/lobby.js
|
||||||
|
|
||||||
|
import { getParam } from "./utils.js";
|
||||||
|
|
||||||
|
const username = getParam("username");
|
||||||
|
|
||||||
|
document.getElementById("createGame").addEventListener("click", () => {
|
||||||
|
if (!username) {
|
||||||
|
alert("Kein Username gefunden!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.href = `/create-game.html?username=${encodeURIComponent(username)}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("joinGame").addEventListener("click", () => {
|
||||||
|
window.location.href = username
|
||||||
|
? `/join-game.html?username=${encodeURIComponent(username)}`
|
||||||
|
: "/join-game.html";
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
// public/js/login.js
|
||||||
|
|
||||||
|
document.getElementById("usernameForm").addEventListener("submit", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const user = document.getElementById("username").value.trim();
|
||||||
|
if (!user) {
|
||||||
|
alert("Bitte gib einen Benutzernamen ein.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.href = `/login?username=${encodeURIComponent(user)}`;
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
// public/js/start-round.js
|
||||||
|
|
||||||
|
import { getParam } from "./utils.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bindet den Klick-Handler an den "Runde starten"-Button,
|
||||||
|
* der per WebSocket an den Server das Start-Event feuert.
|
||||||
|
* @param {WebSocket} socket – die geöffnete WS-Verbindung
|
||||||
|
*/
|
||||||
|
export function setupStartRound(socket) {
|
||||||
|
const gameId = getParam("gameId");
|
||||||
|
if (!gameId || socket.readyState !== WebSocket.OPEN) return;
|
||||||
|
|
||||||
|
const startBtn = document.getElementById("startRound");
|
||||||
|
startBtn.addEventListener("click", () => {
|
||||||
|
// Button direkt deaktivieren, bis neue Runde kommt
|
||||||
|
startBtn.disabled = true;
|
||||||
|
|
||||||
|
// Sende das Start-Runden-Event an den Server
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
type: "start-round",
|
||||||
|
gameId: gameId
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
// public/js/utils.js
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest einen Query-Parameter aus der URL.
|
||||||
|
*/
|
||||||
|
export function getParam(name) {
|
||||||
|
return new URLSearchParams(window.location.search).get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Führt einen Fetch aus und wirft bei HTTP-Fehlern eine Exception.
|
||||||
|
*/
|
||||||
|
export async function fetchJson(url, options) {
|
||||||
|
const res = await fetch(url, options);
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(`${res.status} ${res.statusText}: ${text}`);
|
||||||
|
}
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendert eine Liste von Strings in ein <ul> (oder <ol>), hebt optional ein Item hervor.
|
||||||
|
*/
|
||||||
|
export function renderList(selector, items, highlight) {
|
||||||
|
const ul = document.querySelector(selector);
|
||||||
|
if (!ul) return;
|
||||||
|
ul.innerHTML = "";
|
||||||
|
items.forEach(item => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.textContent = item;
|
||||||
|
if (item === highlight) {
|
||||||
|
li.style.fontWeight = "bold";
|
||||||
|
}
|
||||||
|
ul.append(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Lobby – Spotify Roulette</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Lobby</h1>
|
||||||
|
<p>Was möchtest du tun?</p>
|
||||||
|
<button id="createGame">Spiel erstellen</button>
|
||||||
|
<button id="joinGame">Spiel beitreten</button>
|
||||||
|
|
||||||
|
<script type="module" src="/js/lobby.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
// src/test/java/eric/roulette/service/GameServiceTest.java
|
||||||
|
package eric.Roullette.service;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class GameServiceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetOrCreateGame() {
|
||||||
|
GameService service = new GameService();
|
||||||
|
// Erstes Mal anlegen
|
||||||
|
GameService.Game g1 = service.getOrCreateGame("g1",0);
|
||||||
|
assertNotNull(g1);
|
||||||
|
assertEquals("g1", g1.id());
|
||||||
|
// Beim zweiten Aufruf dieselbe Instanz
|
||||||
|
GameService.Game g2 = service.getOrCreateGame("g1",0);
|
||||||
|
assertSame(g1, g2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Test
|
||||||
|
// void testAddPlayerAndScores() {
|
||||||
|
// GameService service = new GameService();
|
||||||
|
// service.getOrCreateGame("g2"); // Spiel anlegen
|
||||||
|
// service.addPlayer("g2", "Alice"); // Spieler hinzufügen
|
||||||
|
// GameService.Game game = service.getOrCreateGame("g2");
|
||||||
|
// // Spieler-Liste korrekt
|
||||||
|
// assertTrue(game.players().contains("Alice"));
|
||||||
|
// assertEquals(1, game.players().size());
|
||||||
|
// // Score für neuen Spieler initial 0
|
||||||
|
// assertEquals(0, game.scores().get("Alice").intValue());
|
||||||
|
// // Duplikate vermeiden
|
||||||
|
// }
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue