Skip to content

Commit

Permalink
Allow for premature finishing of a round. Allow setting the remaining…
Browse files Browse the repository at this point in the history
… runs to 0 if all players played their solo.
  • Loading branch information
uncaught committed Oct 2, 2020
1 parent c3a7cb5 commit e250b64
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 60 deletions.
13 changes: 9 additions & 4 deletions client/src/pages/round/FinishRound.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@ import {Form} from 'semantic-ui-react';
import {useRound} from '../../store/Rounds';
import {useSortedGames} from '../../store/Games';
import {useFinishRound} from '../../store/Round/FinishRound';
import FinishRoundPrematurely from './FinishRoundPrematurely';

export default function FinishRound(): ReactElement | null {
const round = useRound()!;
const sortedGames = useSortedGames();
const lastGame = sortedGames[sortedGames.length - 1];
const finishRound = useFinishRound();

if (round.endDate || !lastGame || !lastGame.data.isLastGame) {
if (round.endDate || !lastGame) {
return null;
}

return <section>
<Form.Button onClick={finishRound}>Runde Abschließen</Form.Button>
</section>;
if (lastGame.data.isLastGame) {
return <section>
<Form.Button color={'teal'} onClick={finishRound}>Runde abschließen</Form.Button>
</section>;
} else {
return <FinishRoundPrematurely/>;
}
}
40 changes: 40 additions & 0 deletions client/src/pages/round/FinishRoundPrematurely.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React, {ReactElement, useState} from 'react';
import {Button, Form, Header, Icon, Message, Modal} from 'semantic-ui-react';
import {useSortedGames} from '../../store/Games';
import {useFinishRound} from '../../store/Round/FinishRound';
import {usePlayersWithStats} from '../../store/Players';

export default function FinishRoundPrematurely(): ReactElement | null {
const sortedGames = useSortedGames();
const lastGame = sortedGames[sortedGames.length - 1];
const finishRound = useFinishRound();
const [open, setOpen] = useState(false);
const players = usePlayersWithStats();
const playersMissingSolo = players.filter(({dutySoloPlayed}) => !dutySoloPlayed);

return <section>
<Form.Button color={'grey'} size={'mini'} onClick={() => setOpen(true)}>Runde vorzeitig abschließen</Form.Button>

<Modal open={open} onClose={() => setOpen(false)} basic size='small' closeIcon>
<Header>
<Icon name={'flag checkered'}/>
Runde abschließen
</Header>
<Modal.Content>
<p>Möchtest du die Runde jetzt nach {lastGame.gameNumber} Spielen wirklich abschließen?</p>
{playersMissingSolo.length > 0 && <Message error visible>
<Message.Header>Folgende Spieler haben noch nicht ihr Solo gespielt:</Message.Header>
<Message.Content>{playersMissingSolo.map(({member}) => member.name).join(', ')}</Message.Content>
</Message>}
</Modal.Content>
<Modal.Actions>
<Button inverted onClick={() => {
finishRound(true);
setOpen(false);
}}>
<Icon name='checkmark'/> Ja, Runde vorzeitig abschließen
</Button>
</Modal.Actions>
</Modal>
</section>;
}
58 changes: 11 additions & 47 deletions client/src/pages/round/RoundEndInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import React, {ReactElement, useState} from 'react';
import {Button, Divider, Form, Header, Icon, Label, Modal} from 'semantic-ui-react';
import {usePatchRound, useRound} from '../../store/Rounds';
import NumberStepper from '../../components/NumberStepper';
import {useSortedGames} from '../../store/Games';
import {Icon, Label, Modal} from 'semantic-ui-react';
import {useRound} from '../../store/Rounds';
import RoundEndInfoPopup from './RoundEndInfoPopup';

export default function RoundEndInfo(): ReactElement {
const {data} = useRound()!;
const patchRound = usePatchRound();
const sortedGames = useSortedGames();
export default function RoundEndInfo(): ReactElement | null {
const {data, endDate} = useRound()!;
const [open, setOpen] = useState(false);
const [remaining, setRemaining] = useState(1);
const lastGameRunNumber = sortedGames.length ? sortedGames[sortedGames.length - 1].data.runNumber : 1;
if (endDate) {
//Do not display this label if the round has ended. The `data.roundDuration` might not be accurate in case of
// a premature ending.
return null;
}
const endKnown = !data.dynamicRoundDuration || data.roundDuration !== null;
const duration = data.dynamicRoundDuration ? data.roundDuration : 6;

Expand All @@ -26,43 +26,7 @@ export default function RoundEndInfo(): ReactElement {
</div>

<Modal open={open} onClose={() => setOpen(false)} basic size='small' closeIcon>
<Header>
<Icon name={'sync alternate'}/>
Spielende
</Header>
<Modal.Content>
<p>
Wir befinden uns derzeit in Durchgang {lastGameRunNumber}.
</p>

{endKnown && <>
<p>
Das Spiel endet nach {duration} Durchgängen.
</p>
</>}

{data.dynamicRoundDuration && <>
<Divider section/>

<Form className="u-flex-row-around u-align-center">
<Form.Field>
Noch
</Form.Field>
<NumberStepper value={remaining} min={1} onChange={setRemaining} inverted/>
<Form.Field>
volle Durchgänge
</Form.Field>
</Form>
</>}
</Modal.Content>
{data.dynamicRoundDuration && <Modal.Actions>
<Button inverted onClick={() => {
patchRound({data: {roundDuration: lastGameRunNumber + remaining}});
setOpen(false);
}}>
<Icon name='checkmark'/> Ok
</Button>
</Modal.Actions>}
<RoundEndInfoPopup duration={duration} endKnown={endKnown} setOpen={setOpen}/>
</Modal>
</>;
}
56 changes: 56 additions & 0 deletions client/src/pages/round/RoundEndInfoPopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, {ReactElement, useState} from 'react';
import {Button, Divider, Form, Header, Icon, Modal} from 'semantic-ui-react';
import {usePatchRound, useRound} from '../../store/Rounds';
import NumberStepper from '../../components/NumberStepper';
import {useSortedGames} from '../../store/Games';
import {usePlayersWithStats} from '../../store/Players';

export default function RoundEndInfoPopup({duration, endKnown, setOpen}: {
duration: number | null;
endKnown: boolean;
setOpen: (open: boolean) => void;
}): ReactElement {
const {data} = useRound()!;
const patchRound = usePatchRound();
const sortedGames = useSortedGames();
const lastGameRunNumber = sortedGames.length ? sortedGames[sortedGames.length - 1].data.runNumber : 1;
const [remaining, setRemaining] = useState(data.roundDuration ? (data.roundDuration - lastGameRunNumber) : 1);
const playersWithStats = usePlayersWithStats();
const activePlayersWithoutSolo = playersWithStats.filter(
(p) => p.player.leftAfterGameNumber === null && !p.dutySoloPlayed);
const minimumRoundNumbers = activePlayersWithoutSolo.length ? 1 : 0;

return <>
<Header>
<Icon name={'sync alternate'}/>
Spielende
</Header>
<Modal.Content>
<p>Wir befinden uns derzeit in Durchgang {lastGameRunNumber}.</p>

{endKnown && <p>Das Spiel endet nach {duration} Durchgängen.</p>}

{data.dynamicRoundDuration && <>
<Divider section/>

<Form className="u-flex-row-around u-align-center">
<Form.Field>
Noch
</Form.Field>
<NumberStepper value={remaining} min={minimumRoundNumbers} onChange={setRemaining} inverted/>
<Form.Field>
volle Durchgänge
</Form.Field>
</Form>
</>}
</Modal.Content>
{data.dynamicRoundDuration && <Modal.Actions>
<Button inverted onClick={() => {
patchRound({data: {roundDuration: lastGameRunNumber + remaining}});
setOpen(false);
}}>
<Icon name='checkmark'/> Ok
</Button>
</Modal.Actions>}
</>;
}
26 changes: 25 additions & 1 deletion client/src/store/Games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import {
recalcPoints,
reDealGameTypes,
RoundDetailsLoaded,
RoundGames,
RoundsAdd,
RoundsPatch,
} from '@doko/common';
import {usePageContext} from '../Page';
import {useDispatch, useSelector} from 'react-redux';
Expand Down Expand Up @@ -74,16 +76,38 @@ addReducer<GamesRemove>('games/remove', (state, {id, roundId}) => {

addReducer<RoundsAdd>('rounds/add', (state, {round}) => ({...state, [round.id]: {}}));

addReducer<RoundsPatch>('rounds/patch', (state, action) => {
//In case the round was ended prematurely, patch the last game:
if (action.round.endDate) {
const roundGames = state[action.id];
const lastGame = getLastGameOfRoundGames(roundGames);
if (lastGame && !lastGame.data.isLastGame) {
return {
...state,
[action.id]: {
...state[action.id],
[lastGame.id]: mergeStates<Game>(lastGame, {data: {isLastGame: true}}),
},
};
}
}
return state;
});

export const gamesReducer = combinedReducer;

export const gamesSelector = (state: State) => state.games;

function getLastGameOfRoundGames(roundGames: RoundGames): Game | undefined {
return Object.values(roundGames).sort((a, b) => b.gameNumber - a.gameNumber)[0];
}

export function useLatestGroupGame(): Game | undefined {
const round = useLatestGroupRound();
const games = useSelector(gamesSelector);
// eslint-disable-next-line
const roundGames = round && games[round.id] || {};
return Object.values(roundGames).sort((a, b) => b.gameNumber - a.gameNumber)[0];
return getLastGameOfRoundGames(roundGames);
}

export function useSortedGames(): Game[] {
Expand Down
9 changes: 6 additions & 3 deletions client/src/store/Round/FinishRound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import {useHistory} from 'react-router-dom';
import {useCallback} from 'react';
import {RoundResults} from '@doko/common';
import {usePatchRound, useRound} from '../Rounds';
import {useDispatch} from 'react-redux';
import {LoguxDispatch} from '../Logux';

export function useFinishRound() {
const round = useRound();
const dispatch = useDispatch<LoguxDispatch>();
const patchRound = usePatchRound();
const playersWithStats = usePlayersWithStats(true);
const sortedGames = useSortedGames();
const history = useHistory();
const lastGame = sortedGames[sortedGames.length - 1];
return useCallback(() => {
if (!round || !lastGame) {
return useCallback((forcePrematureEnd = false) => {
if (!round || !lastGame || (!lastGame.data.isLastGame && !forcePrematureEnd)) {
return;
}
const results: RoundResults = {
Expand All @@ -29,5 +32,5 @@ export function useFinishRound() {
data: {results},
});
history.push(`/group/${round.groupId}/rounds`);
}, [history, lastGame, patchRound, playersWithStats, round, sortedGames.length]);
}, [dispatch, history, lastGame, patchRound, playersWithStats, round, sortedGames.length]);
}
9 changes: 7 additions & 2 deletions server/src/channels/Games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,15 @@ export async function getGameCountForRound(roundId: string): Promise<number> {
return rows.length ? +rows[0].c : 0;
}

export async function isLastGameOfRound(gameId: string, roundId: string): Promise<boolean> {
export async function getLastGameIdOfRound(roundId: string): Promise<string | null> {
const lastGame = await query<{ id: string }>(`SELECT id FROM games WHERE round_id = ? ORDER BY game_number DESC LIMIT 1`,
[roundId]);
return lastGame.length ? lastGame[0].id === gameId : false;
return lastGame.length ? lastGame[0].id : null;
}

export async function isLastGameOfRound(gameId: string, roundId: string): Promise<boolean> {
const lastGameId = await getLastGameIdOfRound(roundId);
return lastGameId ? lastGameId === gameId : false;
}

server.type<GamesAdd>('games/add', {
Expand Down
14 changes: 11 additions & 3 deletions server/src/channels/Rounds.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import server from '../Server';
import {fromDbValue, getTransactional, insertEntity, query, updateSingleEntity} from '../Connection';
import {createFilter} from '../logux/Filter';
import {Round, RoundsAdd, RoundsLoad, RoundsLoaded, RoundsPatch, RoundsRemove} from '@doko/common';
import {Game, Round, RoundsAdd, RoundsLoad, RoundsLoaded, RoundsPatch, RoundsRemove} from '@doko/common';
import {canEditGroup, canReadGroup} from '../Auth';
import {playersDbConfig, roundsDbConfig} from '../DbTypes';
import {gamesDbConfig, playersDbConfig, roundsDbConfig} from '../DbTypes';
import {memberIdsBelongToGroup} from './GroupMembers';
import {getGameCountForRound} from './Games';
import {getGameCountForRound, getLastGameIdOfRound} from './Games';

export async function getGroupForRound(roundId: string): Promise<string | null> {
const result = await query<{ groupId: string }>(`SELECT group_id as groupId FROM rounds WHERE id = ?`, [roundId]);
Expand Down Expand Up @@ -82,6 +82,14 @@ server.type<RoundsPatch>('rounds/patch', {
},
async process(ctx, action) {
await updateSingleEntity<Round>(ctx.userId!, roundsDbConfig, action.id, action.round);

//In case the round was ended prematurely, patch the last game:
if (action.round.endDate) {
const lastGameId = await getLastGameIdOfRound(action.id);
if (lastGameId) {
await updateSingleEntity<Game>(ctx.userId!, gamesDbConfig, lastGameId, {data: {isLastGame: true}});
}
}
},
});

Expand Down

0 comments on commit e250b64

Please sign in to comment.