Skip to content
This repository has been archived by the owner on Jan 14, 2024. It is now read-only.

Commit

Permalink
Merge branch 'main' into docs/required-documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
arekisanda authored May 16, 2021
2 parents c97b04e + 56de6d0 commit e5338d6
Show file tree
Hide file tree
Showing 20 changed files with 173 additions and 12 deletions.
1 change: 1 addition & 0 deletions backend/grpc/joestar.proto
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ option java_multiple_files = true;

service ChaosConnectService {
rpc StartPlaying(api.game.StartPlayingRequest) returns (api.common.Empty);
rpc StopPlaying(api.common.Empty) returns (api.common.Empty);
rpc PlacePiece(api.game.PlacePieceRequest) returns (api.common.Empty);

rpc GetGameUpdates(api.common.Empty)
Expand Down
1 change: 1 addition & 0 deletions backend/grpc/rohan.proto
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ message GameUpdateResponse {

service GameService {
rpc StartPlaying(api.game.StartPlayingRequest) returns (api.common.Empty);
rpc StopPlaying(api.common.Empty) returns (api.common.Empty);
rpc PlacePiece(api.game.PlacePieceRequest) returns (api.common.Empty);

rpc GetGameUpdates(api.common.Empty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ class ChaosConnectEndpoint(private val gameStateService: GameStateService) :
override suspend fun startPlaying(request: StartPlayingRequest): Empty =
gameStateService.startPlaying(request.faction)
.let { Empty.getDefaultInstance() }

override suspend fun stopPlaying(request: Empty): Empty =
gameStateService.stopPlaying()
.let { Empty.getDefaultInstance() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ interface GameStateService {
*/
suspend fun startPlaying(faction: Faction)

/**
* Stops the game session
*/
suspend fun stopPlaying()

/**
* Indicate whether a healthy connection to the Rohan exists.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ class GameStateServiceImpl(
override suspend fun startPlaying(faction: Faction) =
rohanService.startPlaying(faction = faction)

override suspend fun stopPlaying() {
rohanService.stopPlaying()
}

override fun isConnected() = isConnected.get()

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface RohanService {
fun getGameUpdates(): Flow<GameUpdateResponse>
suspend fun placePiece(column: Int)
suspend fun startPlaying(faction: Faction)
suspend fun stopPlaying()
suspend fun login(username: String, password: String): UserAuthResponse
suspend fun register(
displayName: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ class RohanServiceImpl(
)
}

override suspend fun stopPlaying() {
gameService.stopPlaying(Empty.getDefaultInstance())
}

override suspend fun login(username: String, password: String) =
userService.getUser(
GetUserRequest.newBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class RohanServiceMock : RohanService {
override suspend fun placePiece(column: Int) = Unit

override suspend fun startPlaying(faction: Faction) = Unit
override suspend fun stopPlaying() = Unit

override suspend fun login(
username: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ch.chaosconnect.joestar.endpoints

import ch.chaosconnect.api.authentication.LoginRequest
import ch.chaosconnect.api.common.Empty
import ch.chaosconnect.api.game.PlacePieceRequest
import ch.chaosconnect.api.game.StartPlayingRequest
import ch.chaosconnect.api.joestar.ChaosConnectServiceGrpcKt
Expand Down Expand Up @@ -93,6 +94,13 @@ class JoestarEndpointTest {
)
}
}

@Test
fun `stopPlaying throws unauthenticated exception`(): Unit = runBlocking {
assertThrows<StatusException> {
chaosConnectServiceStub.stopPlaying(Empty.getDefaultInstance())
}
}
}

@Factory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,12 @@ class GameStateServiceTest {
service.startPlaying(Faction.YELLOW)
coVerify { rohanService.startPlaying(faction = Faction.YELLOW) }
}

@Test
fun `stopPlaying is forwarded to Rohan`() =
runBlocking {
coJustRun { rohanService.stopPlaying() }
service.stopPlaying()
coVerify { rohanService.stopPlaying() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ class GameEndpoint(private val service: GameService) :
return Empty.getDefaultInstance()
}

override suspend fun stopPlaying(request: Empty): Empty {
service.stopPlaying()
return Empty.getDefaultInstance()
}

override suspend fun placePiece(request: PlacePieceRequest): Empty {
service.placePiece(columnIndex = request.column)
return Empty.getDefaultInstance()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.Flow

interface GameService {
suspend fun startPlaying(faction: Faction)
suspend fun stopPlaying()
suspend fun placePiece(columnIndex: Int)

fun getGameUpdates(): Flow<Pair<GameUpdateEvent, GameState>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const val initialWidth = 7
const val initialHeight = 6
const val inactiveTimeoutMinutes = 30
const val disabledUntilClearedTimeoutSeconds = 30
const val maxAllowedTeamDifference = 2

private val logger: Logger =
LoggerFactory.getLogger(GameServiceImpl::class.java)
Expand Down Expand Up @@ -52,6 +53,9 @@ class GameServiceImpl(private val storageService: StorageService) :
override suspend fun startPlaying(faction: Faction): Unit = mutex.withLock {
val currentUser = userIdentifierContextKey.get()
?: error("Cannot start playing without a user")

check(playerCanJoin(faction)) { "User can not join unbalanced faction" }

val playerState = activePlayers.compute(currentUser) { _, user ->
when (user) {
null -> ActivePlayerState(
Expand All @@ -76,6 +80,24 @@ class GameServiceImpl(private val storageService: StorageService) :
}
}

private fun playerCanJoin(faction: Faction): Boolean {
val teamSize = activePlayers.values.count { it.faction == faction }
val otherTeamSize = activePlayers.size - teamSize
return teamSize - otherTeamSize < maxAllowedTeamDifference
}

override suspend fun stopPlaying (): Unit = mutex.withLock {
val currentUser = userIdentifierContextKey.get()
val playerState = activePlayers.remove(currentUser) ?: return
emitCurrentState {
playerChanged = PlayerChanged.newBuilder().apply {
action = PlayerAction.DISCONNECT
player = currentUser
state = playerState.toPlayerState()
}.build()
}
}

override suspend fun placePiece(columnIndex: Int): Unit = mutex.withLock {
val currentUser = userIdentifierContextKey.get()
?: error("Cannot place piece without a user")
Expand Down Expand Up @@ -130,7 +152,7 @@ class GameServiceImpl(private val storageService: StorageService) :

val columnIndicesToDisable = mutableSetOf<Int>()

if (column.rows.size == numberOfRows - 1) {
if (column.rows.size == numberOfRows) {
column.queue.clear()
columnIndicesToDisable += columnIndex
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ internal class RohanEndpointTest {
}
}

@Test
fun `stopPlaying stops session`(): Unit =
runBlocking {
coJustRun { gameService.stopPlaying() }
gameServiceStub.stopPlaying(Empty.getDefaultInstance())
coVerify { gameService.stopPlaying() }
}
}

@Factory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ package ch.chaosconnect.rohan.services

import app.cash.turbine.test
import ch.chaosconnect.api.game.*
import ch.chaosconnect.rohan.assertThrowsWithMessage
import ch.chaosconnect.rohan.model.TemporaryUser
import ch.chaosconnect.rohan.model.UserScore
import ch.chaosconnect.rohan.runSignedIn
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import kotlin.IllegalStateException
import kotlin.time.ExperimentalTime

@ExperimentalTime
Expand Down Expand Up @@ -96,6 +99,54 @@ internal class GameServiceImplTest {
}
}

@Test
fun `startPlaying fails on unbalanced team`() {
repeat(maxAllowedTeamDifference) {
val identifier = "user-$it"
val dummyScore = dummyScore(identifier)
every { storage.getUser(identifier) } returns dummyScore
runSignedIn("user-$it") {
service.startPlaying(Faction.RED)
}
}
every { storage.getUser("my-user") } returns dummyScore("my-user")
runSignedIn("my-user") {
assertThrowsWithMessage<IllegalStateException>("User can not join unbalanced faction") {
service.startPlaying(Faction.RED)
}
}
}

@Test
fun `stopPlaying emits DISCONNECT event`() =
runSignedIn("my-user") {
every { storage.getUser("my-user") } returns dummyScore("my-user")
service.getGameUpdates().test {
expectItem()
service.startPlaying(Faction.RED)
expectItem()
service.stopPlaying()
val gameUpdateEvent = expectItem().first
assertEquals(GameUpdateEvent.ActionCase.PLAYER_CHANGED, gameUpdateEvent.actionCase)
val playerChanged = gameUpdateEvent.playerChanged
assertEquals(PlayerAction.DISCONNECT, playerChanged.action)
assertEquals("my-user", playerChanged.player)
expectNoEvents()
cancel()
}
}

@Test
fun `stopPlaying ignores non-playing user`() =
runSignedIn("my-user") {
service.getGameUpdates().test {
expectItem()
service.stopPlaying()
expectNoEvents()
cancel()
}
}

@Test
fun `processQueueTick TODO`() {
// TODO: Add tests
Expand Down
14 changes: 12 additions & 2 deletions frontend/src/components/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@
import twemoji from "../lib/Twemoji";
import TokenRefresher from "./TokenRefresher.svelte";
import LoginRegister from "./LoginRegister.svelte";
import { isLoggedIn, token } from "../stores/Auth";
import { authMetadata, isLoggedIn, token } from "../stores/Auth";
import Game from "./Game.svelte";
import { stopPlaying } from "../lib/ChaosConnectClient";
import { player } from "../stores/GameState";
import { onMount } from "svelte";
function logout(): void {
if ($player) {
stopPlaying($authMetadata);
}
token.unset();
}
</script>

<main>
<h1 use:twemoji>⚔️ ChaosConnect ⚔️</h1>
{#if $isLoggedIn}
<button on:click={() => token.unset()}>Logout</button>
<button on:click={logout}>Logout</button>
<TokenRefresher />
<Game />
{:else}
Expand Down
15 changes: 12 additions & 3 deletions frontend/src/components/FactionSelect.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<script lang="ts">
import type { Faction } from "../gen/game_pb";
import { startPlaying } from "../lib/ChaosConnectClient";
import { factions } from "../lib/GameState";
import { authMetadata } from "../stores/Auth";
import { playerMap, playersByFaction } from "../stores/GameState";
import Piece from "./Piece.svelte";
import Spinner from "./Spinner.svelte";
const MAX_TEAM_DIFFERENCE = 2;
let waiting = false;
let errorOccured = false;
Expand All @@ -22,6 +24,12 @@
console.error(e);
}
}
function isUnbalanced(faction: Faction): boolean {
const teamSize = $playersByFaction.get(faction)?.length ?? 0;
const otherTeamSize = $playerMap.size - teamSize;
return teamSize - otherTeamSize >= MAX_TEAM_DIFFERENCE;
}
</script>

<div class="card">
Expand All @@ -32,9 +40,10 @@
<Spinner />
{:else}
<h2>Choose faction:</h2>
{#each factions as faction}
<button on:click={() => chooseFaction(faction)}>
{#each [...$playersByFaction] as [faction, players]}
<button disabled={isUnbalanced(faction)} on:click={() => chooseFaction(faction)}>
<Piece {faction} />
{players.length} Player{#if players.length !== 1}s{/if}
</button>
{/each}
{/if}
Expand Down
16 changes: 13 additions & 3 deletions frontend/src/components/Game.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import Grid from "./Grid.svelte";
import PlayerList from "./PlayerList.svelte";
import FactionSelect from "./FactionSelect.svelte";
import { getGameUpdates } from "../lib/ChaosConnectClient";
import { getGameUpdates, stopPlaying } from "../lib/ChaosConnectClient";
const TIMEOUT = 500;
Expand Down Expand Up @@ -39,8 +39,18 @@
}
}
onMount(startGameUpdates);
onDestroy(stopGameUpdates);
function unloadListener() {
stopPlaying($authMetadata);
}
onMount(() => {
startGameUpdates();
window.addEventListener('beforeunload', unloadListener)
});
onDestroy(() => {
stopGameUpdates();
window.removeEventListener('beforeunload', unloadListener);
});
</script>

{#if $player}
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/lib/ChaosConnectClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export async function startPlaying(faction: Faction, metadata: Metadata): Promis
return client.startPlaying(request, metadata);
}

export async function stopPlaying(metadata: Metadata): Promise<Empty> {
return client.stopPlaying(new Empty(), metadata);
}

export async function placePiece(column: number, metadata: Metadata): Promise<Empty> {
const request = new PlacePieceRequest();
request.setColumn(column);
Expand Down
11 changes: 8 additions & 3 deletions frontend/src/stores/GameState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,17 @@ export const columns = derived(gameState, $gameState => $gameState.columns);

export const playerMap = derived(gameState, $gameState => $gameState.playerMap);

function comparePlayers(p1: Player, p2: Player): number {
if (p1.disconnected === p2.disconnected) {
return p2.score - p1.score;
}
return p1.disconnected ? 1 : -1;
}

export const playersByFaction = derived(playerMap, $playerMap => {
const result: Map<Faction, Player[]> = new Map(factions.map(f => [f, []]));
$playerMap.forEach(player => result.get(player.faction)!.push(player));
result.forEach(
players => players.sort((p1, p2) => Number(p2.disconnected) - Number(p1.disconnected))
);
result.forEach(players => players.sort(comparePlayers));
return result;
});

Expand Down

0 comments on commit e5338d6

Please sign in to comment.