Skip to content

Commit

Permalink
Merge pull request #406 from wyne/player-sorting
Browse files Browse the repository at this point in the history
Player sorting
  • Loading branch information
wyne authored Apr 21, 2024
2 parents 0ee878e + 82c9a02 commit 3199695
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 66 deletions.
54 changes: 36 additions & 18 deletions redux/GamesSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@ import { getContrastRatio } from 'colorsheet';
import * as Crypto from 'expo-crypto';

import { getPalette } from '../src/ColorPalette';
import { SortDirectionKey, SortSelectorKey } from '../src/components/ScoreLog/SortHelper';
import logger from '../src/Logger';

import { ScoreState, playerAdd, selectAllPlayers, selectPlayerById } from './PlayersSlice';
import { playerAdd, selectPlayerById } from './PlayersSlice';
import { setCurrentGameId } from './SettingsSlice';
import { RootState } from './store';

export interface GameState {
id: string;
title: string;
dateCreated: number;
roundCurrent: number;
roundTotal: number;
roundCurrent: number; // 0-indexed
roundTotal: number; // 1-indexed
playerIds: string[];
locked?: boolean;
sortSelectorKey?: SortSelectorKey;
sortDirectionKey?: SortDirectionKey;
palette?: string;
}

Expand All @@ -26,6 +29,11 @@ const gamesAdapter = createEntityAdapter({
});

const initialState = gamesAdapter.getInitialState({
roundCurrent: 0,
roundTotal: 1,
locked: false,
sortSelectorKey: SortSelectorKey.ByIndex,
sortDirectionKey: SortDirectionKey.Normal,
});

const gamesSlice = createSlice({
Expand Down Expand Up @@ -60,6 +68,26 @@ const gamesSlice = createSlice({
gameDelete(state, action: PayloadAction<string>) {
gamesAdapter.removeOne(state, action.payload);
},
setSortSelector(state, action: PayloadAction<{ gameId: string, sortSelector: SortSelectorKey; }>) {
const game = state.entities[action.payload.gameId];

if (!game) { return; }

let newSortDirection = SortDirectionKey.Normal;

// Toggle sort direction if the same sort selector is selected
if (game.sortSelectorKey === action.payload.sortSelector) {
newSortDirection = game.sortDirectionKey === SortDirectionKey.Normal ? SortDirectionKey.Reversed : SortDirectionKey.Normal;
}

gamesAdapter.updateOne(state, {
id: game.id,
changes: {
sortSelectorKey: action.payload.sortSelector,
sortDirectionKey: newSortDirection,
}
});
},
reorderPlayers(state, action: PayloadAction<{ gameId: string, playerIds: string[]; }>) {
const game = state.entities[action.payload.gameId];
if (!game) { return; }
Expand Down Expand Up @@ -171,21 +199,10 @@ export const asyncCreateGame = createAsyncThunk(
}
);

export const selectSortedPlayers = createSelector(
[
selectAllPlayers,
(state: RootState) => state.settings.currentGameId ? state.games.entities[state.settings.currentGameId] : undefined
],
(players: ScoreState[], currentGame: GameState | undefined) => {
if (!currentGame) return [];

return players.filter(player => currentGame.playerIds?.includes(player.id))
.sort((a, b) => {
if (currentGame?.playerIds == undefined) return 0;
return currentGame.playerIds.indexOf(a.id) - currentGame.playerIds.indexOf(b.id);
});
}
);
export const selectSortSelectorKey = (state: RootState, gameId: string) => {
const key = selectGameById(state, gameId)?.sortSelectorKey;
return key !== undefined ? key : SortSelectorKey.ByScore;
};

const selectPaletteName = (state: RootState, gameId: string) => state.games.entities[gameId]?.palette;
const selectPlayerIndex = (_: RootState, __: string, playerIndex: number) => playerIndex;
Expand Down Expand Up @@ -215,6 +232,7 @@ export const {
roundPrevious,
gameSave,
gameDelete,
setSortSelector,
reorderPlayers,
} = gamesSlice.actions;

Expand Down
2 changes: 1 addition & 1 deletion redux/PlayersSlice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import crashlytics from '@react-native-firebase/crashlytics';
import { createSlice, PayloadAction, createEntityAdapter } from '@reduxjs/toolkit';
import { PayloadAction, createEntityAdapter, createSlice } from '@reduxjs/toolkit';

type RoundIndex = number;

Expand Down
2 changes: 1 addition & 1 deletion redux/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const gamesPersistConfig = {
key: 'games',
version: 0,
storage: AsyncStorage,
whitelist: ['entities', 'ids'],
whitelist: ['entities', 'ids', 'sortSelectorKey'],
};

const playersPersistConfig = {
Expand Down
47 changes: 34 additions & 13 deletions src/components/Rounds.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React, { useEffect, useRef, useState, useCallback } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';

import { ParamListBase } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { View, StyleSheet, ScrollView, Platform, LayoutChangeEvent } from 'react-native';
import { LayoutChangeEvent, Platform, ScrollView, StyleSheet, View } from 'react-native';
import { TouchableOpacity } from 'react-native-gesture-handler';

import { selectGameById } from '../../redux/GamesSlice';
import { useAppSelector } from '../../redux/hooks';
import { selectGameById, selectSortSelectorKey, setSortSelector } from '../../redux/GamesSlice';
import { useAppDispatch, useAppSelector } from '../../redux/hooks';

import PlayerNameColumn from './ScoreLog/PlayerNameColumn';
import RoundScoreColumn from './ScoreLog/RoundScoreColumn';
import { SortSelectorKey, sortSelectors } from './ScoreLog/SortHelper';
import TotalScoreColumn from './ScoreLog/TotalScoreColumn';

interface Props {
Expand All @@ -21,8 +23,6 @@ interface RoundScollOffset {
}

const MemoizedRoundScoreColumn = React.memo(RoundScoreColumn);
const MemoizedTotalScoreColumn = React.memo(TotalScoreColumn);
const MemoizedPlayerNameColumn = React.memo(PlayerNameColumn);

const Rounds: React.FunctionComponent<Props> = ({ }) => {
const [roundScollOffset, setRoundScrollOffset] = useState<RoundScollOffset>({});
Expand Down Expand Up @@ -59,19 +59,40 @@ const Rounds: React.FunctionComponent<Props> = ({ }) => {

const roundsIterator = [...Array(roundTotal).keys()];

const dispatch = useAppDispatch();

const sortSelectorKey = useAppSelector(state => selectSortSelectorKey(state, currentGameId));
const sortSelector = sortSelectors[sortSelectorKey];

const sortByPlayerIndex = () => {
dispatch(setSortSelector({ gameId: currentGameId, sortSelector: SortSelectorKey.ByIndex }));
};

const sortByTotalScore = () => {
dispatch(setSortSelector({ gameId: currentGameId, sortSelector: SortSelectorKey.ByScore }));
};

return (
<View style={[styles.scoreTableContainer]}>
<MemoizedPlayerNameColumn />
<MemoizedTotalScoreColumn />
<TouchableOpacity onPress={sortByPlayerIndex}>
<PlayerNameColumn />
</TouchableOpacity>

<TouchableOpacity onPress={sortByTotalScore}>
<TotalScoreColumn />
</TouchableOpacity>

<ScrollView horizontal={true}
contentContainerStyle={{ flexDirection: 'row' }}
ref={roundsScrollViewEl}>
{roundsIterator.map((item, round) => (
<MemoizedRoundScoreColumn
onLayout={round == roundCurrent ? e => onLayoutHandler(e, round) : undefined}
round={round}
key={round}
isCurrentRound={round == roundCurrent} />
<View key={round} onLayout={e => onLayoutHandler(e, round)}>
<MemoizedRoundScoreColumn
sortSelector={sortSelector}
round={round}
key={round}
isCurrentRound={round == roundCurrent} />
</View>
))}
</ScrollView>
</View>
Expand Down
50 changes: 38 additions & 12 deletions src/components/ScoreLog/PlayerNameColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { selectPlayerColors } from '../../../redux/GamesSlice';
import { useAppSelector } from '../../../redux/hooks';
import { selectPlayerById } from '../../../redux/PlayersSlice';
import { selectCurrentGame } from '../../../redux/selectors';
import { systemBlue } from '../../constants';

import { SortDirectionKey, SortSelectorKey, sortSelectors } from './SortHelper';


interface CellProps {
index: number;
Expand All @@ -28,28 +30,52 @@ const PlayerNameCell: React.FunctionComponent<CellProps> = ({ index, playerId })
);
};

const PlayerHeaderCell: React.FunctionComponent = () => {
const sortKey = useAppSelector(state => selectCurrentGame(state)?.sortSelectorKey);
const sortDirection = useAppSelector(state => selectCurrentGame(state)?.sortDirectionKey);

let sortLabel = '';
if (sortKey === SortSelectorKey.ByIndex && sortDirection === SortDirectionKey.Normal) {
sortLabel = '↓';
} else if (sortKey === SortSelectorKey.ByIndex && sortDirection === SortDirectionKey.Reversed) {
sortLabel = '↑';
}

return (

<Text style={styles.editRow}>
&nbsp;
<Icon name="users"
type="font-awesome-5"
size={19}
color='white' /> {
sortLabel
}
</Text>
);
};


const PlayerNameColumn: React.FunctionComponent = () => {
const playerIds = useAppSelector(state => selectCurrentGame(state)?.playerIds) || [];
const sortKey = useAppSelector(state => selectCurrentGame(state)?.sortSelectorKey);

const sortSelector = sortSelectors[sortKey || SortSelectorKey.ByIndex];
const sortedPlayerIds = useAppSelector(sortSelector);

return (
<View style={{ paddingVertical: 10 }}>
<Text style={styles.editRow}>
&nbsp;
<Icon name="users"
type="font-awesome-5"
size={19}
color='white' />
</Text>
{playerIds.map((playerId, index) => (
<PlayerNameCell key={index} index={index} playerId={playerId} />
<PlayerHeaderCell />

{sortedPlayerIds.map((playerId, index) => (
playerId && <PlayerNameCell key={index} index={index} playerId={playerId} />
))}
</View>
);
};

const styles = StyleSheet.create({
editRow: {
color: systemBlue,
color: 'white',
fontSize: 20,
textAlign: 'center',
}
Expand Down
9 changes: 6 additions & 3 deletions src/components/ScoreLog/RoundScoreColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,30 @@ import { LayoutChangeEvent, Text, TouchableWithoutFeedback, View } from 'react-n

import { updateGame } from '../../../redux/GamesSlice';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
import { selectCurrentGame } from '../../../redux/selectors';
import { RootState } from '../../../redux/store';

import RoundScoreCell from './RoundScoreCell';

interface Props {
round: number;
isCurrentRound: boolean;
disabled?: boolean;
sortSelector: (state: RootState) => string[];
onLayout?: (event: LayoutChangeEvent, round: number) => void;
}

const RoundScoreColumn: React.FunctionComponent<Props> = ({
round,
isCurrentRound,
disabled = false,
sortSelector,
onLayout,
}) => {
const dispatch = useAppDispatch();

const currentGameId = useAppSelector(state => state.settings.currentGameId);
const playerIds = useAppSelector(state => selectCurrentGame(state)?.playerIds) || [];

const sortedPlayerIds = useAppSelector(sortSelector);

const onPressHandler = useCallback(async () => {
if (disabled || !currentGameId) return;
Expand Down Expand Up @@ -67,7 +70,7 @@ const RoundScoreColumn: React.FunctionComponent<Props> = ({
}}>
{round + 1}
</Text>
{playerIds.map((playerId, playerIndex) => (
{sortedPlayerIds.map((playerId, playerIndex) => (
<RoundScoreCell playerId={playerId} round={round} key={playerId} playerIndex={playerIndex} />
))}
</View>
Expand Down
84 changes: 84 additions & 0 deletions src/components/ScoreLog/SortHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { createSelector } from '@reduxjs/toolkit';

import { GameState } from '../../../redux/GamesSlice';
import { ScoreState, selectAllPlayers } from '../../../redux/PlayersSlice';
import { RootState } from '../../../redux/store';

export const selectPlayerIdsByIndex: SortSelector = createSelector(
[
selectAllPlayers,
(state: RootState) => state.settings.currentGameId ? state.games.entities[state.settings.currentGameId] : undefined,
(state: RootState) => state.settings.currentGameId ? state.games.entities[state.settings.currentGameId]?.sortDirectionKey : undefined
],
(players: ScoreState[], currentGame: GameState | undefined) => {
if (!currentGame) return [];

const playerIds = players.filter(player => currentGame.playerIds?.includes(player.id))
.sort((a, b) => {
if (currentGame?.playerIds == undefined) return 0;
return currentGame.playerIds.indexOf(a.id) - currentGame.playerIds.indexOf(b.id);
})
.map(player => player.id);

if (SortDirectionKey.Normal === currentGame.sortDirectionKey) {
return playerIds;
} else {
return playerIds.reverse();
}
}
);

export const selectPlayerIdsByScore: SortSelector = createSelector(
[
selectAllPlayers,
(state: RootState) => state.settings.currentGameId ? state.games.entities[state.settings.currentGameId] : undefined,
(state: RootState) => state.settings.currentGameId ? state.games.entities[state.settings.currentGameId]?.sortDirectionKey : undefined
],
(players: ScoreState[], currentGame: GameState | undefined, sortDirectionKey: SortDirectionKey | undefined) => {
console.log('sort selector');
if (!currentGame) return [];

const playerIds = [...players]
.filter(player => currentGame.playerIds?.includes(player.id))
.sort((a, b) => {
const totalScoreA = a.scores.reduce((acc, score) => acc + score, 0);
const totalScoreB = b.scores.reduce((acc, score) => acc + score, 0);
const scoreDifference = totalScoreB - totalScoreA;

if (scoreDifference === 0) {
// If the total scores are equal, sort by player index
const indexA = currentGame.playerIds?.indexOf(a.id) || 0;
const indexB = currentGame.playerIds?.indexOf(b.id) || 0;
return indexA - indexB;
}

return totalScoreB - totalScoreA;
})
.map(player => player.id);

if (sortDirectionKey === SortDirectionKey.Normal) {
return playerIds;
} else {
return playerIds.reverse();
}
}
);

export interface SortSelector {
(state: RootState): string[];
}

export enum SortSelectorKey {
ByScore = 'byScore',
ByIndex = 'byIndex',
}

export enum SortDirectionKey {
Normal = 'normal',
Reversed = 'reversed',
}

export const sortSelectors = {
[SortSelectorKey.ByScore]: selectPlayerIdsByScore,
[SortSelectorKey.ByIndex]: selectPlayerIdsByIndex,
};
Loading

0 comments on commit 3199695

Please sign in to comment.