Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Leaderboard #855

Merged
merged 25 commits into from
Apr 22, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2a91270
feat(WIP): add leaderboard page skeleton
sgfost Feb 28, 2023
a9ca1ea
Start of leaderboard
Tkawamura02 Mar 3, 2023
2b9fd06
Added b-table to Leaderboard.vue
saachibm Mar 6, 2023
80c377e
Created LeaderboardAPI with AJAX
saachibm Mar 8, 2023
03f3871
Added b-button to Leaderboard.vue
saachibm Mar 8, 2023
eb3fed6
feat(WIP): set up leaderboard service with mock data
sgfost Mar 10, 2023
fc875d7
Reworked on bot checkbox
saachibm Mar 16, 2023
74d1b0c
Todo and meeting updates
Tkawamura02 Mar 17, 2023
d67a430
Added additional properties to Leaderboard.vue
saachibm Mar 23, 2023
13b476a
feat: improve leaderboard view styling/functionality
sgfost Mar 10, 2023
ecaeeb1
feat(WIP): add player statistics page
sgfost Mar 24, 2023
ea8526a
Update sortable
Tkawamura02 Mar 31, 2023
a1458ea
fix: adjust leaderboard highscores query
sgfost Apr 7, 2023
7bfda31
feat: add leaderboard preview to landing page
sgfost Apr 7, 2023
aa312be
chore: add text to landing page community blurb
sgfost Apr 8, 2023
a8cbc25
Reformatted date and layout of player stats
saachibm Apr 13, 2023
17321a6
refactor: rehaul player stats/previous games ui
sgfost Apr 14, 2023
d24a11d
refactor: minor style changes and variable renaming
alee Apr 20, 2023
636b999
refactor: rename copypasta variables
alee Apr 20, 2023
405ad00
fix(build): escape typescript bondage
alee Apr 20, 2023
fb3de05
fix(build): enable yarn style:fix
alee Apr 21, 2023
e3a0514
fix(style): minor hygiene
alee Apr 21, 2023
994f02f
fix: player history assigning wrong winners
sgfost Apr 21, 2023
278b82c
refactor: rename and re-organize leaderboard/game history
sgfost Apr 21, 2023
2ee680e
rename: extract -> get
alee Apr 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,13 @@ settings: $(SENTRY_DSN_PATH) $(SECRET_KEY_PATH) | keys
echo 'export const SENTRY_DSN = "${SENTRY_DSN}";' >> $(SHARED_CONFIG_PATH)


build: docker-compose.yml settings
docker compose build --pull

initialize: build
docker compose run --rm server yarn initdb

docker-compose.yml: base.yml staging.base.yml $(ENVIR).yml config.mk $(DB_DATA_PATH) $(DATA_DUMP_PATH) $(LOG_DATA_PATH) $(REDIS_SETTINGS_PATH) $(ORMCONFIG_PATH) $(NUXT_ORMCONFIG_PATH) $(PGPASS_PATH) $(SERVER_ENV) settings
docker-compose.yml: base.yml $(ENVIR).yml config.mk $(DB_DATA_PATH) $(DATA_DUMP_PATH) $(LOG_DATA_PATH) $(REDIS_SETTINGS_PATH) $(ORMCONFIG_PATH) $(NUXT_ORMCONFIG_PATH) $(PGPASS_PATH) $(SERVER_ENV) settings
case "$(ENVIR)" in \
dev) docker compose -f base.yml -f "$(ENVIR).yml" config > docker-compose.yml;; \
staging|prod) docker compose -f base.yml -f staging.base.yml -f "$(ENVIR).yml" config > docker-compose.yml;; \
staging|prod) docker compose -f base.yml -f staging.yml -f "$(ENVIR).yml" config > docker-compose.yml;; \
*) echo "invalid environment. must be either dev, staging or prod" 1>&2; exit 1;; \
esac

Expand Down
3 changes: 2 additions & 1 deletion base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ services:
build:
context: .
restart: always
image: port-of-mars/server/dev:latest
image: port-of-mars/server:dev
depends_on:
- redis
- db
Expand All @@ -15,6 +15,7 @@ services:
- ./keys:/run/secrets
- ./scripts:/scripts
- ./server/.env:/code/server/.env
- ./.prettierrc:/code/.prettierrc
# XXX: nuxt disabled until typeorm + auth support is properly added
# https://github.com/virtualcommons/port-of-mars/issues/795
# https://github.com/virtualcommons/port-of-mars/issues/809
Expand Down
33 changes: 33 additions & 0 deletions client/src/api/leaderboard/request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { url } from "@port-of-mars/client/util";
import { LeaderboardData, PlayerStatItem } from "@port-of-mars/shared/types";
import { TStore } from "@port-of-mars/client/plugins/tstore";
import { AjaxRequest } from "@port-of-mars/client/plugins/ajax";

export class LeaderboardAPI {
constructor(public store: TStore, public ajax: AjaxRequest) {}

async getLeaderboardData(limit?: number): Promise<LeaderboardData> {
try {
const params = limit ? `?limit=${limit}` : "";
return await this.ajax.get(url(`/leaderboard${params}`), ({ data }) => {
return data;
});
} catch (e) {
console.log("Unable to retrieve leaderboard data");
console.log(e);
throw e;
}
}

async getPlayerStats(): Promise<Array<PlayerStatItem>> {
try {
return await this.ajax.get(url("/leaderboard/stats"), ({ data }) => {
return data;
});
} catch (e) {
console.log("Unable to retrieve player stats");
console.log(e);
throw e;
}
}
}
8 changes: 8 additions & 0 deletions client/src/components/global/Navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
title="Game Manual"
>Manual</b-nav-item
>
<b-nav-item class="mx-2" :to="leaderboard" exact-active-class="active" title="Leaderboard"
>Leaderboard</b-nav-item
>
<b-nav-item class="mx-2" :to="lobby" exact-active-class="active" title="Game Lobby"
>Play</b-nav-item
>
Expand All @@ -53,6 +56,7 @@
<b-icon-person-fill></b-icon-person-fill>
{{ username }}
</template>
<b-dropdown-item :to="playerStats">My Stats</b-dropdown-item>
<b-dropdown-item @click="logout">Sign Out</b-dropdown-item>
</b-nav-item-dropdown>
</div>
Expand All @@ -75,6 +79,8 @@ import {
MANUAL_PAGE,
GAME_PAGE,
LOBBY_PAGE,
PLAYER_STATS_PAGE,
LEADERBOARD_PAGE,
} from "@port-of-mars/shared/routes";
import { isDevOrStaging, Constants } from "@port-of-mars/shared/settings";
import _ from "lodash";
Expand All @@ -100,6 +106,8 @@ export default class Header extends Vue {
login = { name: LOGIN_PAGE };
manual = { name: MANUAL_PAGE };
game = { name: GAME_PAGE };
leaderboard = { name: LEADERBOARD_PAGE };
playerStats = { name: PLAYER_STATS_PAGE };
lobby = { name: LOBBY_PAGE };

async created() {
Expand Down
109 changes: 109 additions & 0 deletions client/src/components/leaderboard/LeaderboardTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<template>
<b-table
dark
sticky-header
no-border-collapse
fixed
striped
:style="`max-height: ${maxHeight}`"
class="h-100 m-0 custom-table"
:fields="showGameStats ? leaderboardFields.concat(gameStatsFields) : leaderboardFields"
:items="showWithBots ? leaderboardData.withBots : leaderboardData.withoutBots"
sort-by="rank"
:sort-asc="true"
sort-icon-left
>
<!-- headers -->
<template #head(username)> Player </template>
<template #head(points)>
Points
<small>
<b-icon-question-circle id="points-tooltip" class="ml-2" scale="1" />
</small>
<b-tooltip target="points-tooltip" placement="top" variant="light">
The total number of Victory Points a player has earned in games where the entire group
survived.
</b-tooltip>
</template>
<template #head(victoryPercentage)>
Victory %
<small>
<b-icon-question-circle id="victory-percentage-tooltip" class="ml-2" scale="1" />
</small>
<b-tooltip target="victory-percentage-tooltip" placement="top" variant="light">
Percentage of games that ended in victory that this player has participated in (not
necessarily as the highest scoring player).
</b-tooltip>
</template>
<template #head(totalGames)> Games Played </template>
<!-- cells -->
<template #cell(rank)="data">
{{ "#" + data.item.rank }}
</template>
<template #cell(victoryPercentage)="data"> {{ getVictoryPercentage(data.item) }}% </template>
<template #cell(totalGames)="data">
{{ getTotalGamesPlayed(data.item) }}
</template>
</b-table>
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { LeaderboardAPI } from "@port-of-mars/client/api/leaderboard/request";
import { LeaderboardData } from "@port-of-mars/shared/types";

@Component({})
export default class Leaderboard extends Vue {
@Prop({ default: false }) showWithBots!: boolean;
@Prop({ default: 50 }) limit!: number;
@Prop({ default: true }) showGameStats!: boolean;
@Prop({ default: "none" }) maxHeight!: string;

api!: LeaderboardAPI;

leaderboardData: LeaderboardData = {
withBots: [],
withoutBots: [],
};
leaderboardFields = [{ key: "rank", sortable: true }, { key: "username" }, { key: "points" }];
gameStatsFields = [{ key: "victoryPercentage" }, { key: "totalGames" }];

async created() {
await this.fetchLeaderboardData();
}

async fetchLeaderboardData() {
this.api = new LeaderboardAPI(this.$store, this.$ajax);
this.leaderboardData = await this.api.getLeaderboardData(this.limit);
// highlight top player(s)
const noBots = this.leaderboardData.withoutBots;
if (noBots && noBots.length > 0) {
const topPlayer = noBots[0];
const maxPoints = topPlayer.points;
for (const player of noBots) {
if (player.points < maxPoints) break;
// FIXME: look into better color palette
(player as any)._rowVariant = "success";
}
}
}

extractWinsLosses(item: any) {
return {
wins: parseInt(item.wins),
losses: parseInt(item.losses),
};
}

getVictoryPercentage(item: any) {
const { wins, losses } = this.extractWinsLosses(item);
const percentage = (wins / (wins + losses)) * 100;
return percentage === 100 ? 100 : percentage.toFixed(0);
}

getTotalGamesPlayed(item: any) {
const { wins, losses } = this.extractWinsLosses(item);
return wins + losses;
}
}
</script>
76 changes: 41 additions & 35 deletions client/src/components/leaderboard/PlayerStatItem.vue
Original file line number Diff line number Diff line change
@@ -1,46 +1,52 @@
<template>
<b-container fluid class="h-100 m-0 p-0">
<b-row>
<b-col>
<h4>
<b-badge :variant="status === 'Victory' ? 'success' : 'danger'">{{ status }}</b-badge>
<!-- Round {{ playerStatItem.round }} -->
<span class="float-right">{{ dateString }}</span>
</h4>
<b-list-group horizontal>
<b-list-group-item
v-for="playerScore in playerStatItem.playerScores"
:key="playerScore.role"
class="d-flex flex-fill justify-content-between align-items-center"
variant="light"
>
<span class="score">
{{ playerScore.role }}
<template v-if="playerScore.isSelf">(YOUR ROLE)</template>
</span>
<span class="score mx-2">
<b-badge pill :variant="getVariant(playerStatItem.victory, playerScore.winner)"
>{{ playerScore.points }} points</b-badge
>
</span>
</b-list-group-item>
</b-list-group>
</b-col>
</b-row>
</b-container>
<b-row>
<b-col>
<h4>
<b-badge class="">{{ formattedDate }} </b-badge>
</h4>
<b-list-group horizontal class="content-container mb-3">
<b-list-group-item :class="status === 'Victory' ? 'bg-success' : 'bg-politician'">
<span class="score">
{{ status }}
</span>
</b-list-group-item>
<b-list-group-item
v-for="playerScore in playerStatItem.playerScores"
:key="playerScore.role"
class="d-flex flex-fill justify-content-between align-items-center bg-dark"
>
<span class="score">
{{ playerScore.role }}
<b-icon-person-check-fill v-if="playerScore.isSelf" />
</span>
<span class="score mx-2">
<b-badge pill :variant="getVariant(playerStatItem.victory, playerScore.winner)"
>{{ playerScore.points }} points</b-badge
>
</span>
</b-list-group-item>
</b-list-group>
</b-col>
</b-row>
</template>

<script lang="ts">
import { Vue, Component, Prop } from "vue-property-decorator";
import * as types from "@port-of-mars/shared/types";
import { PlayerStatItem } from "@port-of-mars/shared/types";

@Component({})
export default class PlayerStatItem extends Vue {
@Prop() private playerStatItem!: types.PlayerStatItem;
export default class PlayerStatGame extends Vue {
@Prop() playerStatItem!: PlayerStatItem;

get dateString() {
const date = new Date(this.playerStatItem.time);
return date.toString();
get formattedDate() {
return new Date(this.playerStatItem.time).toLocaleDateString("en-US", {
weekday: "short",
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
});
}

getActive(victory: boolean, winner: boolean) {
Expand Down
6 changes: 6 additions & 0 deletions client/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import Rooms from "@port-of-mars/client/views/admin/Rooms.vue";
import Reports from "@port-of-mars/client/views/admin/Reports.vue";
import Settings from "@port-of-mars/client/views/admin/Settings.vue";
import Login from "@port-of-mars/client/views/#.vue";
import Leaderboard from "@port-of-mars/client/views/Leaderboard.vue";
import PlayerStats from "@port-of-mars/client/views/PlayerStats.vue";
import Lobby from "@port-of-mars/client/views/Lobby.vue";
import LobbyRoom from "@port-of-mars/client/components/lobby/LobbyRoom.vue";
import LobbyRoomList from "@port-of-mars/client/components/lobby/LobbyRoomList.vue";
Expand All @@ -23,6 +25,8 @@ import {
LOGIN_PAGE,
LOBBY_PAGE,
GAME_PAGE,
LEADERBOARD_PAGE,
PLAYER_STATS_PAGE,
REGISTER_PAGE,
VERIFY_PAGE,
MANUAL_PAGE,
Expand Down Expand Up @@ -66,6 +70,8 @@ const router = new VueRouter({
],
},
{ ...PAGE_META[GAME_PAGE], component: Game },
{ ...PAGE_META[LEADERBOARD_PAGE], component: Leaderboard },
{ ...PAGE_META[PLAYER_STATS_PAGE], component: PlayerStats },
{ ...PAGE_META[REGISTER_PAGE], component: Register },
{ ...PAGE_META[VERIFY_PAGE], component: Verify },
{ ...PAGE_META[MANUAL_PAGE], component: Manual },
Expand Down
11 changes: 11 additions & 0 deletions client/src/stylesheets/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,17 @@ button {
}
}

.custom-table {
@extend .table-dark;
th {
@extend h4;
font-size: 1.1rem;
}
td {
@extend p;
}
}

// override bootstrap-vue dark text
.text-dark {
color: $white !important;
Expand Down
Loading