Skip to content

Commit

Permalink
Merge pull request #76 from Chr7000/levels
Browse files Browse the repository at this point in the history
Add level tiers to achievements
  • Loading branch information
tyilo authored Aug 22, 2024
2 parents 5681eb8 + 536df36 commit 1ed8400
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 51 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@v3

- name: Pull existing images
run: docker-compose pull
run: docker compose pull

- name: Build image
run: ./build
Expand Down Expand Up @@ -92,15 +92,15 @@ jobs:
- name: Load docker image
run: docker load < image.tar.gz

- name: Test docker-compose
- name: Test docker compose
run: |
for k in "SECRET_KEY" "EMAIL_HOST_PASSWORD" "FACEBOOK_ACCESS_TOKEN"; do
echo "$k=foo" >> ".env"
done
docker network create traefik
docker-compose up -d
docker compose up -d
sleep 10
res=$(docker-compose ps)
res=$(docker compose ps)
echo "$res"
if echo "$res" | grep Exit &>/dev/null; then
exit 1
Expand Down
144 changes: 102 additions & 42 deletions games/achievements.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime
import zoneinfo
from enum import StrEnum

from django.db.models import Q

Expand All @@ -11,13 +12,25 @@
TIMEZONE_FILENAME = zoneinfo._tzpath.find_tzfile("Europe/Copenhagen")
with open(TIMEZONE_FILENAME, "rb") as f:
TIMEZONE_DATA = zoneinfo._common.load_data(f)

VALID_DATES = filter(lambda t: t > 0, TIMEZONE_DATA[1])
DST_TRANSITION_TIMES = [
datetime.datetime.fromtimestamp(t, tz=datetime.timezone.utc)
for t in TIMEZONE_DATA[1]
datetime.datetime.fromtimestamp(t, tz=datetime.timezone.utc) for t in VALID_DATES
]


class AchievementLevel(StrEnum):
"""Each level must correspond to a style in styles.css"""

GOLD = "gold"
SILVER = "silver"
BRONZE = "bronze"
BASE = "base"
NO_LEVEL = "no_level"

def __gt__(self, other):
return list(AchievementLevel).index(self) < list(AchievementLevel).index(other)


class AchievementMetaClass(type):
def __init__(self, name, *args, **kwargs):
super().__init__(name, *args, **kwargs)
Expand All @@ -29,7 +42,7 @@ class Achievement(metaclass=AchievementMetaClass):
__slots__ = ["name", "description", "icon"]

@staticmethod
def has_achieved(user):
def get_level(user):
raise NotImplementedError

@staticmethod
Expand All @@ -42,101 +55,148 @@ class DNFAchievement(Achievement):
description = "Participated in a game that completed, where you didn't"
icon = "coffin.svg"

def has_achieved(user):
return (
def get_level(user):
if (
user.gameplayer_set.filter(dnf=True)
.filter(game__dnf=False, game__end_datetime__isnull=False)
.exists()
)
):
return AchievementLevel.GOLD
return AchievementLevel.NO_LEVEL


class Top10Achievement(Achievement):
name = "Top 10"
description = "Placed top 10 total sips in a season"
class TopAchievement(Achievement):
name = "Top"
description = "Placed top (10/5/3/1) total sips in a season"
icon = "trophy-cup.svg"

def has_achieved(user):
def get_level(user):
current_season = Season.current_season()
highest_tier = AchievementLevel.NO_LEVEL
for i in range(1, current_season.number): # Exclude current season
top10 = (
top10 = list(
PlayerStat.objects.filter(season_number=i)
.order_by("-total_sips")[:10]
.values("user")
)
if {"user": user.id} in top10:
return True

return False
try:
rank = top10.index({"user": user.id})
if rank < 1:
return AchievementLevel.GOLD
elif rank < 3:
highest_tier = max(AchievementLevel.SILVER, highest_tier)
elif rank < 5:
highest_tier = max(AchievementLevel.BRONZE, highest_tier)
elif rank < 10:
highest_tier = max(AchievementLevel.BASE, highest_tier)
except ValueError:
continue
return highest_tier


class FastGameAchievement(Achievement):
name = "Fast Game"
description = "Finished a game in less than 30 minutes"
description = "Finished a game in less than (30/20/15/10) minutes"
icon = "stopwatch.svg"

def has_achieved(user):
return (
Game.add_durations(
Game.objects.filter(
gameplayer__in=user.gameplayer_set.filter(dnf=False).filter(
game__dnf=False
)
)
def get_level(user):
short_games = Game.add_durations(
Game.objects.filter(
gameplayer__in=user.gameplayer_set.filter(dnf=False, game__dnf=False)
)
.filter(duration__lt=datetime.timedelta(minutes=30))
.exists()
)
).filter(duration__lt=datetime.timedelta(minutes=30))
if short_games.filter(duration__lt=datetime.timedelta(minutes=10)).exists():
return AchievementLevel.GOLD
elif short_games.filter(duration__lt=datetime.timedelta(minutes=15)).exists():
return AchievementLevel.SILVER
elif short_games.filter(duration__lt=datetime.timedelta(minutes=20)).exists():
return AchievementLevel.BRONZE
elif short_games.exists():
return AchievementLevel.BASE
else:
return AchievementLevel.NO_LEVEL


class DanishDSTAchievement(Achievement):
name = "DST"
description = "Participated in a game, while a DST transition happened in Denmark"
icon = "backward-time.svg"

def has_achieved(user):
def get_level(user):
query = Q()
for dt in DST_TRANSITION_TIMES:
query |= Q(start_datetime__lt=dt, end_datetime__gt=dt)

return user.games.filter(query).exists()
if user.games.filter(query).exists():
return AchievementLevel.BASE
return AchievementLevel.NO_LEVEL


class TheBarrelAchievement(Achievement):
name = "The Barrel"
description = "Consumed 100 beers in-game"
description = "Consumed (100/1000/2000/4000) beers in-game"
icon = "barrel.svg"

def has_achieved(user):
return (user.stats_for_season(all_time_season).total_sips / 14) >= 100
def get_level(user):
total_sips = user.stats_for_season(all_time_season).total_sips / 14
if total_sips >= 4000:
return AchievementLevel.GOLD
elif total_sips >= 2000:
return AchievementLevel.SILVER
elif total_sips >= 1000:
return AchievementLevel.BRONZE
elif total_sips >= 100:
return AchievementLevel.BASE
return AchievementLevel.NO_LEVEL


class BundeCampAchievement(Achievement):
name = "Chug Camp"
description = "Got 50 chugs in-game"
description = "Got (50/100/250/500) chugs in-game"
icon = "ace.svg"

def has_achieved(user):
return user.stats_for_season(all_time_season).total_chugs >= 50
def get_level(user):
total_chugs = user.stats_for_season(all_time_season).total_chugs
if total_chugs >= 500:
return AchievementLevel.GOLD
elif total_chugs >= 250:
return AchievementLevel.SILVER
elif total_chugs >= 100:
return AchievementLevel.BRONZE
elif total_chugs >= 50:
return AchievementLevel.BASE
return AchievementLevel.NO_LEVEL


class StudyHardAchievement(Achievement):
name = "Study Hard"
description = (
"Spend at least the amount of time corresponding to 2.5 ECTS in game (56 hours)"
"Spend at least the amount of time corresponding to (2.5/15/30/60) ECTS in game"
)
icon = "diploma.svg"

def has_achieved(user):
return user.stats_for_season(all_time_season).approx_ects >= 2.5
def get_level(user):
ects = user.stats_for_season(all_time_season).approx_ects
if ects >= 60:
return AchievementLevel.GOLD
elif ects >= 30:
return AchievementLevel.SILVER
elif ects >= 15:
return AchievementLevel.BRONZE
elif ects >= 2.5:
return AchievementLevel.BASE
return AchievementLevel.NO_LEVEL


class PilfingerAchievement(Achievement):
name = "Pilfinger"
description = "Stille stille stille, Pille pille pille"
icon = "pilfinger.jpg"

def has_achieved(user):
return user.id == 2030
def get_level(user):
if user.id == 2030:
return AchievementLevel.GOLD
return AchievementLevel.NO_LEVEL

def is_hidden(user):
return not PilfingerAchievement.has_achieved(user)
return PilfingerAchievement.get_level(user) != AchievementLevel.GOLD
25 changes: 23 additions & 2 deletions web/static/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,32 @@ tr.in-game a {
margin: 12px;
height: 90px;
width: 90px;
background-color: black;
}

.achievements img.no_level {
opacity: 0.2;
}

.achievements img.active {
opacity: 1;
.achievements img.base {
}

.achievements img.bronze {
border-color: #cd7f32;
border-style: solid;
border-width: 5px;
}

.achievements img.silver {
border-color: #c0c0c0;
border-style: solid;
border-width: 5px;
}

.achievements img.gold {
border-color: #d4af37;
border-style: solid;
border-width: 5px;
}

.invalid-feedback {
Expand Down
2 changes: 1 addition & 1 deletion web/templates/player_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ <h4><i class="fas fa-trophy"></i> Achievements</h4>
<div class="container">
<div class="row achievements">
{% for achievement in achievements %}
<img class="rounder{% if achievement.achieved %} active{% endif %}" src="{{ achievement.icon_url }}" title="{{ achievement.name }}: {{ achievement.description }}">
<img class="rounder {{ achievement.level }}" src="{{ achievement.icon_url }}" title="{{ achievement.name }}: {{ achievement.description }}">
{% endfor %}
</div>
</div>
Expand Down
3 changes: 1 addition & 2 deletions web/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from collections import Counter
from collections.abc import Iterable
from urllib.parse import urlencode

from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.mixins import LoginRequiredMixin
Expand Down Expand Up @@ -292,7 +291,7 @@ def get_context_data(self, **kwargs):

context["achievements"].append(
{
"achieved": achievement.has_achieved(self.object),
"level": achievement.get_level(self.object),
"name": achievement.name,
"description": achievement.description,
"icon_url": static(f"achievements/{achievement.icon}"),
Expand Down

0 comments on commit 1ed8400

Please sign in to comment.