diff --git a/build-all-dc.sh b/build-all-dc.sh index d3fae8cdc..5fb9ba051 100755 --- a/build-all-dc.sh +++ b/build-all-dc.sh @@ -12,5 +12,6 @@ docker-compose build judge docker-compose build changelog docker-compose build history-provider docker-compose build game-lobby +docker-compose build color-chooser docker-compose build gateway docker-compose build kafkacat diff --git a/build-all-local.sh b/build-all-local.sh index f1c2a48a9..6f9f7a3f4 100755 --- a/build-all-local.sh +++ b/build-all-local.sh @@ -4,6 +4,7 @@ docker-compose build judge docker-compose build changelog docker-compose build history-provider docker-compose build game-lobby +docker-compose build color-chooser docker-compose build kafkacat cd gateway && cargo clean && cd .. && docker-compose build gateway docker-compose build kafkacat diff --git a/build-giant-dc.sh b/build-giant-dc.sh index aef253600..e06778c65 100755 --- a/build-giant-dc.sh +++ b/build-giant-dc.sh @@ -3,6 +3,7 @@ docker-compose -f dc-giant.yml build judge docker-compose -f dc-giant.yml build changelog docker-compose -f dc-giant.yml build game-lobby +docker-compose -f dc-giant.yml build color-chooser docker-compose -f dc-giant.yml build history-provider docker-compose -f dc-giant.yml build kafkacat diff --git a/color-chooser/Dockerfile b/color-chooser/Dockerfile new file mode 100644 index 000000000..deebaa229 --- /dev/null +++ b/color-chooser/Dockerfile @@ -0,0 +1,11 @@ +FROM gradle + +RUN curl -O https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh + +RUN chmod 755 wait-for-it.sh + +COPY . . + +RUN sh build.sh + +CMD ["sh", "run.sh"] diff --git a/color-chooser/build.gradle b/color-chooser/build.gradle new file mode 100644 index 000000000..fcc6161cf --- /dev/null +++ b/color-chooser/build.gradle @@ -0,0 +1,61 @@ +plugins { + id 'java' + id 'org.jetbrains.kotlin.jvm' version '1.3.41' +} + +group 'farm.terkwood' +version '0.0.1' + +apply plugin: 'kotlin' +apply plugin: 'application' + +mainClassName = 'ApplicationKt' + +defaultTasks 'run' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +test { useJUnitPlatform() } +ext.junitVersion = '5.4.2' + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + compile "org.jetbrains.kotlin:kotlin-reflect:1.3.0" + + // JSON serialization + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.7' + compile 'com.fasterxml.jackson.module:jackson-module-kotlin:2.9.7' + + compile 'org.apache.kafka:kafka-streams:2.3.0' + + + testCompile "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testImplementation( + 'org.assertj:assertj-core:3.12.2', + "org.junit.jupiter:junit-jupiter-api:$junitVersion" + ) + testRuntime("org.junit.jupiter:junit-jupiter-engine:$junitVersion") + testCompile 'org.apache.kafka:kafka-streams-test-utils:2.3.0' + testCompile group: 'org.rocksdb', name: 'rocksdbjni', version: '6.0.1' + + +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +// fat jar +jar { + manifest { attributes 'Main-Class': 'ApplicationKt' } + from { + configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } + } +} diff --git a/color-chooser/build.sh b/color-chooser/build.sh new file mode 100644 index 000000000..9394be12b --- /dev/null +++ b/color-chooser/build.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +PROJ_NAME="bugout.color-chooser" + +rm -f ./build/libs/$PROJ_NAME*.jar +gradle build +cp ./build/libs/$PROJ_NAME*.jar $PROJ_NAME.jar diff --git a/color-chooser/gradle.properties b/color-chooser/gradle.properties new file mode 100644 index 000000000..29e08e8ca --- /dev/null +++ b/color-chooser/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official \ No newline at end of file diff --git a/color-chooser/gradle/wrapper/gradle-wrapper.jar b/color-chooser/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..87b738cbd Binary files /dev/null and b/color-chooser/gradle/wrapper/gradle-wrapper.jar differ diff --git a/color-chooser/gradle/wrapper/gradle-wrapper.properties b/color-chooser/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..3b544c362 --- /dev/null +++ b/color-chooser/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Aug 20 11:01:53 EDT 2019 +distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/color-chooser/gradlew b/color-chooser/gradlew new file mode 100755 index 000000000..af6708ff2 --- /dev/null +++ b/color-chooser/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +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" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/color-chooser/gradlew.bat b/color-chooser/gradlew.bat new file mode 100644 index 000000000..0f8d5937c --- /dev/null +++ b/color-chooser/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/color-chooser/run.sh b/color-chooser/run.sh new file mode 100644 index 000000000..66e8cddb3 --- /dev/null +++ b/color-chooser/run.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +./wait-for-it.sh kafka:9092 -s -- sleep 16 +java -jar bugout.color-chooser.jar diff --git a/color-chooser/settings.gradle b/color-chooser/settings.gradle new file mode 100644 index 000000000..acf43a38f --- /dev/null +++ b/color-chooser/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'bugout.color-chooser' + diff --git a/color-chooser/src/main/kotlin/AggregatedPrefs.kt b/color-chooser/src/main/kotlin/AggregatedPrefs.kt new file mode 100644 index 000000000..108a3ab1f --- /dev/null +++ b/color-chooser/src/main/kotlin/AggregatedPrefs.kt @@ -0,0 +1,7 @@ +import serdes.jsonMapper + +class AggregatedPrefs { + var prefs: ArrayList = arrayListOf() + fun add(p: ClientGameColorPref) = prefs.add(p) + fun asByteArray(): ByteArray = jsonMapper.writeValueAsBytes(this) +} \ No newline at end of file diff --git a/color-chooser/src/main/kotlin/Aliases.kt b/color-chooser/src/main/kotlin/Aliases.kt new file mode 100644 index 000000000..b8e46e168 --- /dev/null +++ b/color-chooser/src/main/kotlin/Aliases.kt @@ -0,0 +1,6 @@ +import java.util.UUID + + +typealias ClientId = UUID +typealias GameId = UUID +typealias EventId = UUID diff --git a/color-chooser/src/main/kotlin/Application.kt b/color-chooser/src/main/kotlin/Application.kt new file mode 100644 index 000000000..da5e530e7 --- /dev/null +++ b/color-chooser/src/main/kotlin/Application.kt @@ -0,0 +1,195 @@ +import org.apache.kafka.common.serialization.Serdes +import org.apache.kafka.common.utils.Bytes +import org.apache.kafka.streams.* +import org.apache.kafka.streams.kstream.* +import org.apache.kafka.streams.state.KeyValueStore +import serdes.* +import java.time.temporal.ChronoUnit +import java.util.* + +fun main() { + TimeZone.setDefault(TimeZone.getTimeZone("UTC")) + Application("kafka:9092").process() +} + +const val REQUIRED_PREFS = 2 + +class Application(private val brokers: String) { + fun process() { + val topology = build() + + println(topology.describe()) + + val props = Properties() + props[StreamsConfig.BOOTSTRAP_SERVERS_CONFIG] = brokers + props[StreamsConfig.APPLICATION_ID_CONFIG] = "bugout-color-chooser" + props[StreamsConfig.PROCESSING_GUARANTEE_CONFIG] = "exactly_once" + + val streams = KafkaStreams(topology, props) + streams.start() + } + + + fun build(): Topology { + val streamsBuilder = StreamsBuilder() + + buildGameColorPref(streamsBuilder) + + val aggregated = aggregateColorPrefs(streamsBuilder) + + val readyToChoose: KStream = + aggregated.toStream() + .filter { _, agg -> agg.prefs.size == REQUIRED_PREFS } + + readyToChoose.mapValues { agg -> + ColorsChosen.resolve(agg.prefs[0], agg.prefs[1]) + }.mapValues { v -> + println("🎨 ${v.gameId.short()} COLRCHSN $v") + jsonMapper.writeValueAsString(v) + }.to( + Topics.COLORS_CHOSEN, + Produced.with(Serdes.UUID(), Serdes.String()) + ) + + return streamsBuilder.build() + } + + + private fun buildGameColorPref(streamsBuilder: StreamsBuilder) { + val chooseColorPref: KStream = + streamsBuilder.stream( + Topics.CHOOSE_COLOR_PREF, + Consumed.with(Serdes.UUID(), Serdes.String()) + ) + .mapValues { v -> + jsonMapper.readValue( + v, + ChooseColorPref::class.java + ) + } + + val gameReady: KStream = + streamsBuilder.stream( + Topics.GAME_READY, + Consumed.with(Serdes.UUID(), Serdes.String()) + ) + .mapValues { v -> + jsonMapper.readValue( + v, + GameReady::class.java + ) + } + + // generate a ClientGameReady event for the first client + gameReady + .map { _, gr -> + KeyValue( + gr.clients.first, + ClientGameReady(gr.clients.first, gr.gameId) + ) + } + .mapValues { cgr -> jsonMapper.writeValueAsString(cgr) } + .to( + Topics.CLIENT_GAME_READY, + Produced.with(Serdes.UUID(), Serdes.String()) + ) + + // generate a ClientGameReady event for the second client + gameReady + .map { _, gr -> + KeyValue( + gr.clients.second, + ClientGameReady(gr.clients.second, gr.gameId) + ) + } + .mapValues { cgr -> jsonMapper.writeValueAsString(cgr) } + .to( + Topics.CLIENT_GAME_READY, + Produced.with(Serdes.UUID(), Serdes.String()) + ) + + + val clientGameReady: KStream = + streamsBuilder.stream( + Topics.CLIENT_GAME_READY, + Consumed.with(Serdes.UUID(), Serdes.String()) + ) + .mapValues { v -> + jsonMapper.readValue( + v, + ClientGameReady::class.java + ) + } + + val prefJoiner: ValueJoiner = + ValueJoiner { leftValue: ClientGameReady, + rightValue: ChooseColorPref -> + ClientGameColorPref( + leftValue.clientId, leftValue.gameId, + rightValue.colorPref + ) + } + + + val clientGameColorPref: KStream = + clientGameReady.join( + chooseColorPref, prefJoiner, + JoinWindows.of(ChronoUnit.YEARS.duration), + Joined.with( + Serdes.UUID(), + Serdes.serdeFrom( + ClientGameReadySer(), + ClientGameReadyDes() + ), + Serdes.serdeFrom(ChooseColorPrefSer(), ChooseColorPrefDes()) + ) + ) + + val gameColorPref = clientGameColorPref + .map { _, gcp -> + KeyValue( + gcp.gameId, + gcp + ) + } + + // these will be used to aggregate prefs + gameColorPref.mapValues { v -> jsonMapper.writeValueAsString(v) }.to( + Topics.GAME_COLOR_PREF, + Produced.with(Serdes.UUID(), Serdes.String()) + ) + + } + + + private fun aggregateColorPrefs( + streamsBuilder: StreamsBuilder + ): KTable = + streamsBuilder + .stream( + Topics.GAME_COLOR_PREF, + Consumed.with(Serdes.UUID(), Serdes.String()) + ).groupByKey(Serialized.with(Serdes.UUID(), Serdes.String())) + .aggregate({ AggregatedPrefs() }, + { _, p, allPrefs -> + allPrefs.add( + jsonMapper.readValue( + p, + ClientGameColorPref::class.java + ) + ) + allPrefs + }, + Materialized.`as`>( + Topics.COLOR_PREFS_STORE + ) + .withKeySerde(Serdes.UUID()) + .withValueSerde( + Serdes.serdeFrom( + AggPrefSer(), + AggPrefDes() + ) + ) + ) +} diff --git a/color-chooser/src/main/kotlin/ChooseColorPref.kt b/color-chooser/src/main/kotlin/ChooseColorPref.kt new file mode 100644 index 000000000..27b419ce3 --- /dev/null +++ b/color-chooser/src/main/kotlin/ChooseColorPref.kt @@ -0,0 +1,9 @@ +import serdes.jsonMapper + +/** A client's chosen color preference. Keyed by client ID */ +data class ChooseColorPref( + val clientId: ClientId, + val colorPref: ColorPref +){ + fun asByteArray(): ByteArray = jsonMapper.writeValueAsBytes(this) +} diff --git a/color-chooser/src/main/kotlin/ClientGameColorPref.kt b/color-chooser/src/main/kotlin/ClientGameColorPref.kt new file mode 100644 index 000000000..52a9871f4 --- /dev/null +++ b/color-chooser/src/main/kotlin/ClientGameColorPref.kt @@ -0,0 +1,6 @@ +import serdes.jsonMapper + +/** result of a join */ +data class ClientGameColorPref(val clientId: ClientId, + val gameId: GameId, + val colorPref: ColorPref) \ No newline at end of file diff --git a/color-chooser/src/main/kotlin/ClientGameReady.kt b/color-chooser/src/main/kotlin/ClientGameReady.kt new file mode 100644 index 000000000..715f1d89f --- /dev/null +++ b/color-chooser/src/main/kotlin/ClientGameReady.kt @@ -0,0 +1,11 @@ +import serdes.jsonMapper + +/** Emitted downstream of GameReady. Client ID is + * topic key. + */ +data class ClientGameReady ( + val clientId: ClientId, + val gameId: GameId +){ + fun asByteArray(): ByteArray = jsonMapper.writeValueAsBytes(this) +} diff --git a/color-chooser/src/main/kotlin/ColorPref.kt b/color-chooser/src/main/kotlin/ColorPref.kt new file mode 100644 index 000000000..64946ebf6 --- /dev/null +++ b/color-chooser/src/main/kotlin/ColorPref.kt @@ -0,0 +1,7 @@ +enum class ColorPref { + Black, + White, + Any +} + +fun isAny(c : ColorPref): Boolean = c == ColorPref.Any \ No newline at end of file diff --git a/color-chooser/src/main/kotlin/ColorsChosen.kt b/color-chooser/src/main/kotlin/ColorsChosen.kt new file mode 100644 index 000000000..c59fdf5ce --- /dev/null +++ b/color-chooser/src/main/kotlin/ColorsChosen.kt @@ -0,0 +1,62 @@ +import kotlin.random.Random + +data class ColorsChosen(val gameId: GameId, val black: ClientId, val white: ClientId) { + + companion object { + fun resolve(first: ClientGameColorPref, second: ClientGameColorPref): ColorsChosen { + val noConflict: Boolean by lazy { first.colorPref != second.colorPref } + val bf: ColorsChosen by lazy { blackFirst(first, second) } + val wf: ColorsChosen by lazy { whiteFirst(first, second) } + return when { + isAny(first.colorPref) -> + when (force(second.colorPref)) { + Color.Black -> wf + Color.White -> bf + } + isAny(second.colorPref) -> + when (force(first.colorPref)) { + Color.Black -> bf + Color.White -> wf + } + first.colorPref == ColorPref.Black && noConflict -> + bf + first.colorPref == ColorPref.White && noConflict -> + wf + // both sides picked the same color + else -> + when (random()) { + Color.Black -> blackFirst(first, second) + Color.White -> whiteFirst(first, second) + } + } + } + + private fun blackFirst(first: ClientGameColorPref, second: ClientGameColorPref) = + ColorsChosen( + gameId = first.gameId, + black = first.clientId, + white = second.clientId + ) + + private fun whiteFirst(first: ClientGameColorPref, second: ClientGameColorPref) = + ColorsChosen( + gameId = first.gameId, + black = second.clientId, + white = first.clientId + ) + + private fun random(): Color = when (Random.nextBoolean()) { + false -> Color.Black + true -> Color.White + } + + private fun force(cp: ColorPref): Color = + when (cp) { + ColorPref.Any -> random() + ColorPref.Black -> Color.Black + ColorPref.White -> Color.White + } + } +} + +enum class Color { Black, White } \ No newline at end of file diff --git a/color-chooser/src/main/kotlin/GameColorPref.kt b/color-chooser/src/main/kotlin/GameColorPref.kt new file mode 100644 index 000000000..09493ce6f --- /dev/null +++ b/color-chooser/src/main/kotlin/GameColorPref.kt @@ -0,0 +1,12 @@ +/** Represents an individual client's color + * preference for a specific game. Used in + * multiple contexts: initially, this can + * be keyed by client ID as the result of + * a join against ClientGameReady + * + * This can also be an input to the topic + * which aggregates color prefs for a given + * game + */ +data class GameColorPref (val clientId: ClientId, val gameId: GameId, + val colorPref: ColorPref) \ No newline at end of file diff --git a/color-chooser/src/main/kotlin/GameReady.kt b/color-chooser/src/main/kotlin/GameReady.kt new file mode 100644 index 000000000..d653546a2 --- /dev/null +++ b/color-chooser/src/main/kotlin/GameReady.kt @@ -0,0 +1,4 @@ +/** emitted by game-lobby */ +data class GameReady(val gameId: GameId, + val clients: Pair, + val eventId: EventId) diff --git a/color-chooser/src/main/kotlin/Topics.kt b/color-chooser/src/main/kotlin/Topics.kt new file mode 100644 index 000000000..f8552ebf8 --- /dev/null +++ b/color-chooser/src/main/kotlin/Topics.kt @@ -0,0 +1,8 @@ +object Topics { + const val CHOOSE_COLOR_PREF = "bugout-choose-color-pref-cmd" + const val GAME_READY = "bugout-game-ready-ev" + const val CLIENT_GAME_READY = "bugout-client-game-ready-ev" + const val GAME_COLOR_PREF = "bugout-game-color-pref-ev" + const val COLOR_PREFS_STORE = "bugout-color-prefs-store" + const val COLORS_CHOSEN = "bugout-colors-chosen-ev" +} \ No newline at end of file diff --git a/color-chooser/src/main/kotlin/serdes/AggPrefSerde.kt b/color-chooser/src/main/kotlin/serdes/AggPrefSerde.kt new file mode 100644 index 000000000..7c7ee2cb3 --- /dev/null +++ b/color-chooser/src/main/kotlin/serdes/AggPrefSerde.kt @@ -0,0 +1,32 @@ +package serdes + +import AggregatedPrefs +import com.fasterxml.jackson.core.type.TypeReference + +import org.apache.kafka.common.errors.SerializationException +import org.apache.kafka.common.serialization.Deserializer +import org.apache.kafka.common.serialization.Serializer +import java.lang.RuntimeException + +class AggPrefSer : Serializer { + override fun serialize(topic: String?, data: AggregatedPrefs?): ByteArray? { + if (data == null) {return null} + + try {return data.asByteArray()}catch(e: RuntimeException) { + throw SerializationException("error serializing aggregated prefs", e) + } + } +} + +class AggPrefDes : Deserializer { + override fun deserialize(topic: String?, data: ByteArray?): AggregatedPrefs? { + if (data == null) {return null} + + try { + return jsonMapper.readValue(data, AggregatedPrefs::class.java) + } catch (e: RuntimeException) { + throw SerializationException("error deserializing aggregated prefs", e) + } + } + +} \ No newline at end of file diff --git a/color-chooser/src/main/kotlin/serdes/ChooseColorPrefDes.kt b/color-chooser/src/main/kotlin/serdes/ChooseColorPrefDes.kt new file mode 100644 index 000000000..19cd76915 --- /dev/null +++ b/color-chooser/src/main/kotlin/serdes/ChooseColorPrefDes.kt @@ -0,0 +1,26 @@ +package serdes + +import ChooseColorPref +import org.apache.kafka.common.errors.SerializationException +import org.apache.kafka.common.serialization.Deserializer + +class ChooseColorPrefDes : Deserializer { + + override fun configure(configs: Map, isKey: Boolean) {} + + override fun close() {} + + override fun deserialize(topic: String, bytes: ByteArray?): ChooseColorPref? { + if (bytes == null) { + return null + } + + try { + return jsonMapper.readValue(bytes, ChooseColorPref::class.java) + } catch (e: RuntimeException) { + throw SerializationException("Error deserializing value", e) + } + + } + +} \ No newline at end of file diff --git a/color-chooser/src/main/kotlin/serdes/ChooseColorPrefSer.kt b/color-chooser/src/main/kotlin/serdes/ChooseColorPrefSer.kt new file mode 100644 index 000000000..4a9904b34 --- /dev/null +++ b/color-chooser/src/main/kotlin/serdes/ChooseColorPrefSer.kt @@ -0,0 +1,28 @@ +package serdes + + +import ChooseColorPref +import org.apache.kafka.common.errors.SerializationException +import org.apache.kafka.common.serialization.Serializer + + +class ChooseColorPrefSer : Serializer { + + override fun configure(configs: Map, isKey: Boolean) {} + + override fun close() {} + + override fun serialize(topic: String, logAgg: ChooseColorPref?): ByteArray? { + if (logAgg == null) { + return null + } + + try { + return logAgg.asByteArray() + } catch (e: RuntimeException) { + throw SerializationException("Error serializing value", e) + } + + } + +} diff --git a/color-chooser/src/main/kotlin/serdes/ClientGameReadyDes.kt b/color-chooser/src/main/kotlin/serdes/ClientGameReadyDes.kt new file mode 100644 index 000000000..c7c1e9f0f --- /dev/null +++ b/color-chooser/src/main/kotlin/serdes/ClientGameReadyDes.kt @@ -0,0 +1,29 @@ +package serdes + + +import ClientGameReady +import org.apache.kafka.common.errors.SerializationException +import org.apache.kafka.common.serialization.Deserializer + +class ClientGameReadyDes : Deserializer { + + override fun configure(configs: Map, isKey: Boolean) {} + + override fun close() {} + + override fun deserialize(topic: String, bytes: ByteArray?): ClientGameReady? { + if (bytes == null) { + return null + } + + try { + return jsonMapper.readValue(bytes, ClientGameReady::class.java) + } catch (e: RuntimeException) { + throw SerializationException("Error deserializing value", e) + } + + } + +} + + diff --git a/color-chooser/src/main/kotlin/serdes/ClientGameReadySer.kt b/color-chooser/src/main/kotlin/serdes/ClientGameReadySer.kt new file mode 100644 index 000000000..2cc1f0903 --- /dev/null +++ b/color-chooser/src/main/kotlin/serdes/ClientGameReadySer.kt @@ -0,0 +1,26 @@ +package serdes + +import ClientGameReady +import org.apache.kafka.common.errors.SerializationException +import org.apache.kafka.common.serialization.Serializer + +class ClientGameReadySer : Serializer { + + override fun configure(configs: Map, isKey: Boolean) {} + + override fun close() {} + + override fun serialize(topic: String, data: ClientGameReady?): ByteArray? { + if (data == null) { + return null + } + + try { + return data.asByteArray() + } catch (e: RuntimeException) { + throw SerializationException("Error serializing value", e) + } + + } + +} \ No newline at end of file diff --git a/color-chooser/src/main/kotlin/serdes/jsonMapper.kt b/color-chooser/src/main/kotlin/serdes/jsonMapper.kt new file mode 100644 index 000000000..612c5bee7 --- /dev/null +++ b/color-chooser/src/main/kotlin/serdes/jsonMapper.kt @@ -0,0 +1,14 @@ +package serdes + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.util.StdDateFormat +import com.fasterxml.jackson.module.kotlin.registerKotlinModule + + +val jsonMapper = ObjectMapper().apply { + registerKotlinModule() + disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + dateFormat = StdDateFormat() +} + diff --git a/color-chooser/src/main/kotlin/shortUuid.kt b/color-chooser/src/main/kotlin/shortUuid.kt new file mode 100644 index 000000000..d3605009e --- /dev/null +++ b/color-chooser/src/main/kotlin/shortUuid.kt @@ -0,0 +1,4 @@ +import java.util.* + +const val SIZE = 8 +fun UUID.short(): String = this.toString().take(SIZE) diff --git a/color-chooser/src/test/kotlin/Setup.kt b/color-chooser/src/test/kotlin/Setup.kt new file mode 100644 index 000000000..c421421a0 --- /dev/null +++ b/color-chooser/src/test/kotlin/Setup.kt @@ -0,0 +1,17 @@ +import org.apache.kafka.common.serialization.Serdes +import org.apache.kafka.streams.StreamsConfig +import org.apache.kafka.streams.TopologyTestDriver +import java.util.* + +fun setup(): TopologyTestDriver { + // setup test driver + val props = Properties() + props[StreamsConfig.BOOTSTRAP_SERVERS_CONFIG] = "no-matter" + props[StreamsConfig.APPLICATION_ID_CONFIG] = "test-bugout-color-chooser" + props[StreamsConfig.PROCESSING_GUARANTEE_CONFIG] = "exactly_once" + + + val topology = Application("dummy-brokers").build() + println(topology.describe()) + return TopologyTestDriver(topology, props) +} \ No newline at end of file diff --git a/color-chooser/src/test/kotlin/TestChoice.kt b/color-chooser/src/test/kotlin/TestChoice.kt new file mode 100644 index 000000000..0499419f3 --- /dev/null +++ b/color-chooser/src/test/kotlin/TestChoice.kt @@ -0,0 +1,226 @@ +import org.apache.kafka.clients.producer.ProducerRecord +import org.apache.kafka.common.serialization.* +import org.apache.kafka.streams.TopologyTestDriver +import org.apache.kafka.streams.test.ConsumerRecordFactory +import org.apache.kafka.streams.test.OutputVerifier +import org.junit.jupiter.api.* +import serdes.jsonMapper +import java.util.* + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TestChoice { + private val testDriver: TopologyTestDriver = setup() + + private fun push( + c1: ChooseColorPref, + c2: ChooseColorPref, + gameId: GameId + ): ProducerRecord? { + + val gameReadyEvent = GameReady( + gameId, + Pair(c1.clientId, c2.clientId), + eventId = UUID.randomUUID() + ) + + val factory = + ConsumerRecordFactory( + UUIDSerializer(), StringSerializer() + ) + + + testDriver.pipeInput( + factory.create( + Topics.CHOOSE_COLOR_PREF, + c1.clientId, + jsonMapper.writeValueAsString(c1) + ) + ) + + testDriver.pipeInput( + factory.create( + Topics.CHOOSE_COLOR_PREF, + c2.clientId, + jsonMapper.writeValueAsString(c2) + ) + ) + + + testDriver.pipeInput( + factory.create( + Topics.GAME_READY, + gameId, + jsonMapper.writeValueAsString(gameReadyEvent) + ) + ) + + return testDriver.readOutput( + Topics.COLORS_CHOSEN, + UUIDDeserializer(), + StringDeserializer() + ) + + + } + + @Test + fun testNoConflict() { + + val clientOne = UUID.randomUUID() + val clientTwo = UUID.randomUUID() + val gameId = UUID.randomUUID() + + val c1Pref = ChooseColorPref(clientOne, ColorPref.White) + val c2Pref = ChooseColorPref(clientTwo, ColorPref.Black) + + val chosen = push( + c1Pref, + c2Pref, gameId + ) + + + OutputVerifier.compareKeyValue( + chosen, gameId, + jsonMapper.writeValueAsString( + ColorsChosen( + gameId = gameId, + black = clientTwo, + white = clientOne + ) + ) + ) + } + + @Test + fun testAnotherNoConflict() { + val clientOne = UUID.randomUUID() + val clientTwo = UUID.randomUUID() + val gameId = UUID.randomUUID() + + val chosen = push( + ChooseColorPref(clientOne, ColorPref.Black), + ChooseColorPref(clientTwo, ColorPref.White), gameId + ) + + OutputVerifier.compareKeyValue( + chosen, gameId, + jsonMapper.writeValueAsString( + ColorsChosen( + gameId = gameId, + black = clientOne, + white = clientTwo + ) + ) + ) + } + + @Test + fun testConflict() { + val clientOne = UUID.randomUUID() + val clientTwo = UUID.randomUUID() + val gameId = UUID.randomUUID() + + val chosen: ColorsChosen = jsonMapper.readValue(push( + ChooseColorPref(clientOne, ColorPref.Black), + ChooseColorPref(clientTwo, ColorPref.Black), gameId + )?.value(), ColorsChosen::class.java) + + Assertions.assertTrue(when (chosen.black) { + clientOne -> chosen.white == clientTwo + else -> chosen.black == clientTwo && chosen.white == clientOne + }) + } + + @Test + fun testMoreConflict() { + val clientOne = UUID.randomUUID() + val clientTwo = UUID.randomUUID() + val gameId = UUID.randomUUID() + + val chosen: ColorsChosen = jsonMapper.readValue(push( + ChooseColorPref(clientOne, ColorPref.White), + ChooseColorPref(clientTwo, ColorPref.White), gameId + )?.value(), ColorsChosen::class.java) + + Assertions.assertTrue(when (chosen.black) { + clientOne -> chosen.white == clientTwo + else -> chosen.black == clientTwo && chosen.white == clientOne + }) + } + + @Test + fun testSimpleDemands() { + val clientOne = UUID.randomUUID() + val clientTwo = UUID.randomUUID() + val gameId = UUID.randomUUID() + + val chosen = push( + ChooseColorPref(clientOne, ColorPref.Any), + ChooseColorPref(clientTwo, ColorPref.White), gameId + ) + + OutputVerifier.compareKeyValue( + chosen, gameId, + jsonMapper.writeValueAsString( + ColorsChosen( + gameId = gameId, + black = clientOne, + white = clientTwo + ) + ) + ) + } + + @Test + fun testMoreDemands() { + val clientOne = UUID.randomUUID() + val clientTwo = UUID.randomUUID() + val gameId = UUID.randomUUID() + + val chosen = push( + ChooseColorPref(clientOne, ColorPref.White), + ChooseColorPref(clientTwo, ColorPref.Any), gameId + ) + + OutputVerifier.compareKeyValue( + chosen, gameId, + jsonMapper.writeValueAsString( + ColorsChosen( + gameId = gameId, + black = clientTwo, + white = clientOne + ) + ) + ) + } + + @Test + fun testLooseConcerns() { + val clientOne = UUID.randomUUID() + val clientTwo = UUID.randomUUID() + val gameId = UUID.randomUUID() + + val chosenJson = push( + ChooseColorPref(clientOne, ColorPref.Any), + ChooseColorPref(clientTwo, ColorPref.Any), gameId + ) + + val chosen: ColorsChosen = + jsonMapper.readValue(chosenJson?.value(), ColorsChosen::class.java) + + when (chosen.black) { + clientOne -> Assertions.assertTrue(chosen.white == clientTwo) + else -> Assertions.assertTrue( + chosen.black == clientTwo && + chosen.white == clientOne + ) + } + + } + + + @AfterAll + fun tearDown() { + testDriver.close() + } +} \ No newline at end of file diff --git a/color-chooser/src/test/kotlin/TestJoin.kt b/color-chooser/src/test/kotlin/TestJoin.kt new file mode 100644 index 000000000..a586bef37 --- /dev/null +++ b/color-chooser/src/test/kotlin/TestJoin.kt @@ -0,0 +1,92 @@ +import org.apache.kafka.common.serialization.* +import org.apache.kafka.streams.TopologyTestDriver +import org.apache.kafka.streams.test.ConsumerRecordFactory +import org.junit.jupiter.api.* +import serdes.jsonMapper +import java.util.* + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TestJoin { + private val testDriver: TopologyTestDriver = setup() + + @Test + fun testJoin() { + + val clientOne = UUID.randomUUID() + val clientTwo = UUID.randomUUID() + val gameId = UUID.randomUUID() + + val clientOnePref = ChooseColorPref(clientOne, ColorPref.Black) + val clientTwoPref = ChooseColorPref(clientTwo, ColorPref.White) + + val gameReadyEvent = GameReady(gameId, Pair(clientOne, clientTwo), eventId = UUID.randomUUID()) + + val factory = + ConsumerRecordFactory( + UUIDSerializer(), StringSerializer() + ) + + + testDriver.pipeInput( + factory.create( + Topics.CHOOSE_COLOR_PREF, + clientOne, + jsonMapper.writeValueAsString(clientOnePref) + ) + ) + + testDriver.pipeInput( + factory.create( + Topics.CHOOSE_COLOR_PREF, + clientTwo, + jsonMapper.writeValueAsString(clientTwoPref) + ) + ) + + + testDriver.pipeInput( + factory.create( + Topics.GAME_READY, + gameId, + jsonMapper.writeValueAsString(gameReadyEvent) + ) + ) + + + val gcpOne = + testDriver.readOutput( + Topics.GAME_COLOR_PREF, + UUIDDeserializer(), + StringDeserializer() + ) + + + val gcpTwo = + testDriver.readOutput( + Topics.GAME_COLOR_PREF, + UUIDDeserializer(), + StringDeserializer() + ) + + + val gameColorPrefs = + listOf(gcpOne.value(), gcpTwo.value()) + .map { jsonMapper.readValue(it, GameColorPref::class.java) } + + val expectedSize = 2 + Assertions.assertEquals(expectedSize, + gameColorPrefs.size) + + Assertions.assertArrayEquals( + listOf( + GameColorPref(clientOne,gameId,clientOnePref.colorPref), + GameColorPref(clientTwo,gameId,clientTwoPref.colorPref) + ).toTypedArray(), + gameColorPrefs.toTypedArray()) + } + + @AfterAll + fun tearDown() { + testDriver.close() + } +} \ No newline at end of file diff --git a/color-chooser/topology.jpg b/color-chooser/topology.jpg new file mode 100644 index 000000000..88069b789 Binary files /dev/null and b/color-chooser/topology.jpg differ diff --git a/dc-giant.yml b/dc-giant.yml index 6507686c5..3ffa5c6aa 100644 --- a/dc-giant.yml +++ b/dc-giant.yml @@ -12,7 +12,7 @@ services: KAFKA_ADVERTISED_HOST_NAME: kafka KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092 - KAFKA_CREATE_TOPICS: "bugout-move-accepted-ev:1:1,bugout-move-made-ev:1:1,bugout-move-rejected-ev:1:1,bugout-make-move-cmd:1:1,bugout-game-states:1:1,bugout-provide-history-cmd:1:1,bugout-history-provided-ev:1:1,bugout-find-public-game-cmd:1:1,bugout-create-game-cmd:1:1,bugout-wait-for-opponent-ev:1:1,bugout-game-ready-ev:1:1,bugout-private-game-rejected-ev:1:1,bugout-join-private-game-cmd:1:1,bugout-game-lobby-commands:1:1" + KAFKA_CREATE_TOPICS: "bugout-move-accepted-ev:1:1,bugout-move-made-ev:1:1,bugout-move-rejected-ev:1:1,bugout-make-move-cmd:1:1,bugout-game-states:1:1,bugout-provide-history-cmd:1:1,bugout-history-provided-ev:1:1,bugout-find-public-game-cmd:1:1,bugout-create-game-cmd:1:1,bugout-wait-for-opponent-ev:1:1,bugout-game-ready-ev:1:1,bugout-private-game-rejected-ev:1:1,bugout-join-private-game-cmd:1:1,bugout-game-lobby-commands:1:1,bugout-choose-color-pref-cmd:1:1,bugout-client-game-ready-ev:1:1,bugout-game-color-pref-ev:1:1,bugout-colors-chosen-ev:1:1" KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 volumes: - /var/run/docker.sock:/var/run/docker.sock @@ -40,6 +40,12 @@ services: - "kafka" depends_on: - "kafka" + color-chooser: + build: color-chooser/. + links: + - "kafka" + depends_on: + - "kafka" startup: build: startup/. links: diff --git a/docker-compose.yml b/docker-compose.yml index bfd352638..934c011ba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,7 @@ services: KAFKA_ADVERTISED_HOST_NAME: kafka KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092 - KAFKA_CREATE_TOPICS: "bugout-move-accepted-ev:1:1,bugout-move-made-ev:1:1,bugout-move-rejected-ev:1:1,bugout-make-move-cmd:1:1,bugout-game-states:1:1,bugout-provide-history-cmd:1:1,bugout-history-provided-ev:1:1,bugout-find-public-game-cmd:1:1,bugout-create-game-cmd:1:1,bugout-wait-for-opponent-ev:1:1,bugout-game-ready-ev:1:1,bugout-private-game-rejected-ev:1:1,bugout-join-private-game-cmd:1:1,bugout-game-lobby-commands:1:1" + KAFKA_CREATE_TOPICS: "bugout-move-accepted-ev:1:1,bugout-move-made-ev:1:1,bugout-move-rejected-ev:1:1,bugout-make-move-cmd:1:1,bugout-game-states:1:1,bugout-provide-history-cmd:1:1,bugout-history-provided-ev:1:1,bugout-find-public-game-cmd:1:1,bugout-create-game-cmd:1:1,bugout-wait-for-opponent-ev:1:1,bugout-game-ready-ev:1:1,bugout-private-game-rejected-ev:1:1,bugout-join-private-game-cmd:1:1,bugout-game-lobby-commands:1:1,bugout-choose-color-pref-cmd:1:1,bugout-client-game-ready-ev:1:1,bugout-game-color-pref-ev:1:1,bugout-colors-chosen-ev:1:1" KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 volumes: - /var/run/docker.sock:/var/run/docker.sock @@ -56,6 +56,12 @@ services: - "kafka" depends_on: - "kafka" + color-chooser: + build: color-chooser/. + links: + - "kafka" + depends_on: + - "kafka" startup: build: startup/. links: diff --git a/game-lobby/gradle/wrapper/gradle-wrapper.properties b/game-lobby/gradle/wrapper/gradle-wrapper.properties index 44e7c4d1d..d1bcfc0a0 100644 --- a/game-lobby/gradle/wrapper/gradle-wrapper.properties +++ b/game-lobby/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Mon Aug 19 06:42:51 EDT 2019 +distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/kafkacat/color-chooser.sh b/kafkacat/color-chooser.sh new file mode 100644 index 000000000..9882d04be --- /dev/null +++ b/kafkacat/color-chooser.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# need a gameready event to have been emitted by game lobby +# keyed by game ID +echo 'ffff0000-bbab-bbbb-5434-b63e326006bd:{"gameId":"ffff0000-bbab-bbbb-5434-b63e326006bd", "clients": {"first":"eeeeeeee-fefe-1234-ffff-d5f736841a5f","second":"bbbbbbbb-8787-6a6a-5432-bce65874eed2"}, "eventId":"3f1eac1e-ed30-4fdf-811a-eaf9eba01cc5"}' | kafkacat -b kafka:9092 -t bugout-game-ready-ev -K: -P + + +# each player chooses a color pref +# keyed by client ID +echo 'eeeeeeee-fefe-1234-ffff-d5f736841a5f:{"clientId":"eeeeeeee-fefe-1234-ffff-d5f736841a5f", "colorPref":"Black"}' | kafkacat -b kafka:9092 -t bugout-choose-color-pref-cmd -K: -P +echo 'bbbbbbbb-8787-6a6a-5432-bce65874eed2:{"clientId":"bbbbbbbb-8787-6a6a-5432-bce65874eed2", "colorPref":"Black"}' | kafkacat -b kafka:9092 -t bugout-choose-color-pref-cmd -K: -P + + +# wait for the magic to happen +# keyed by game ID +kafkacat -b kafka:9092 -t bugout-colors-chosen-ev -C -K: