Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Othello Rating System #41

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ service_identity = "~=18.1.0"
social-auth-app-django = "~=4.0.0"
sqlparse = "~=0.4.2"
pyopenssl = "*"
django-celery-beat = "*"

[dev-packages]
flake8 = "~=3.9.2"
Expand Down
491 changes: 267 additions & 224 deletions Pipfile.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions celerybeat-schedule.bak
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'entries', (0, 417)
'__version__', (512, 15)
'tz', (1024, 26)
'utc_enabled', (1536, 4)
Binary file added celerybeat-schedule.dat
Binary file not shown.
4 changes: 4 additions & 0 deletions celerybeat-schedule.dir
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'entries', (0, 417)
'__version__', (512, 15)
'tz', (1024, 26)
'utc_enabled', (1536, 4)
2 changes: 2 additions & 0 deletions manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import os
import sys

import sys
print(sys.version)

def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'othello.settings')
Expand Down
18 changes: 18 additions & 0 deletions othello/apps/auth/migrations/0008_user_is_gauntlet_running.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.25 on 2024-05-25 02:04

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('authentication', '0007_auto_20210422_1152'),
]

operations = [
migrations.AddField(
model_name='user',
name='is_gauntlet_running',
field=models.BooleanField(default=False),
),
]
18 changes: 18 additions & 0 deletions othello/apps/auth/migrations/0009_user_last_gauntlet_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.25 on 2024-05-28 03:17

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('authentication', '0008_user_is_gauntlet_running'),
]

operations = [
migrations.AddField(
model_name='user',
name='last_gauntlet_run',
field=models.DateTimeField(null=True),
),
]
4 changes: 4 additions & 0 deletions othello/apps/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ class User(AbstractUser):
is_student = models.BooleanField(default=True, null=False)
is_imported = models.BooleanField(default=False, null=False)

# rating
is_gauntlet_running = models.BooleanField(default=False, null=False)
last_gauntlet_run = models.DateTimeField(null=True)

@property
def has_management_permission(self) -> bool:
return self.is_teacher or self.is_staff or self.is_superuser
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Generated by Django 3.2.25 on 2024-07-17 00:40

from django.db import migrations, models


class Migration(migrations.Migration):

replaces = [('games', '0031_auto_20240523_1555'), ('games', '0032_submission_gauntlet'), ('games', '0033_game_is_ranked'), ('games', '0034_auto_20240525_2248'), ('games', '0035_auto_20240526_1514'), ('games', '0036_game_is_gauntlet')]

dependencies = [
('games', '0030_auto_20220804_1438'),
]

operations = [
migrations.AddField(
model_name='submission',
name='rating',
field=models.IntegerField(default=400),
),
migrations.AddField(
model_name='submission',
name='gauntlet',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='game',
name='is_ranked',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='game',
name='ratingDelta',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='game',
name='blackRating',
field=models.IntegerField(default=0, null=True),
),
migrations.AddField(
model_name='game',
name='whiteRating',
field=models.IntegerField(default=0, null=True),
),
migrations.AddField(
model_name='game',
name='is_gauntlet',
field=models.BooleanField(default=False),
),
]
24 changes: 23 additions & 1 deletion othello/apps/games/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.contrib.auth import get_user_model
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.db.models import Q
from django.db.models import Q, Subquery
from django.utils import timezone

from ...moderator.constants import Player
Expand All @@ -28,6 +28,17 @@ def latest(self, **kwargs: Any) -> "models.query.QuerySet[Submission]":
"""
return self.filter(**kwargs).order_by("user", "-created_at").distinct("user")

def rated(self, **kwargs: Any) -> "models.query.QuerySet[Submission]":
"""
Returns a set of all the rated submissions for all users
"""
# return self.filter(**kwargs).order_by("user", "-created_at").distinct("user").order_by("-rating")
g1 = self.latest() # Submission.objects.filter(user__in=Subquery(self.latest().values('user')))
g2 = self.filter(gauntlet=True)
g3 = g2.intersection(g1)
g3 = g3.order_by("-rating")
return g3


class Submission(models.Model):

Expand All @@ -38,6 +49,10 @@ class Submission(models.Model):
created_at = models.DateTimeField(auto_now=True)
code = models.FileField(upload_to=_save_path, default=None)

# Rating Info
rating = models.IntegerField(default=400, null=False)
gauntlet = models.BooleanField(default=False, null=False)

is_legacy = models.BooleanField(default=False)
tournament_win_year = models.IntegerField(default=-1)

Expand Down Expand Up @@ -102,8 +117,15 @@ class Game(models.Model):
forfeit = models.BooleanField(default=False)
outcome = models.CharField(max_length=1, choices=OUTCOME_CHOICES, default="T")
score = models.IntegerField(default=0)
ratingDelta = models.IntegerField(default=0)

# rating values for both sides before rating delta is applied
blackRating = models.IntegerField(null=True, default=0)
whiteRating = models.IntegerField(null=True, default=0)

is_tournament = models.BooleanField(default=False)
is_ranked = models.BooleanField(default=False)
is_gauntlet = models.BooleanField(default=False)
playing = models.BooleanField(default=False)
last_heartbeat = models.DateTimeField(default=timezone.now)

Expand Down
173 changes: 91 additions & 82 deletions othello/apps/games/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ def send_through_game_channel(game: Game, event_type: str, object_id: int) -> in


def check_heartbeat(game: Game) -> bool:
if game.is_tournament:
if game.is_tournament or game.is_ranked or game.is_gauntlet:
return True
game.refresh_from_db()
return (timezone.now() - game.last_heartbeat).seconds < settings.CLIENT_HEARTBEAT_INTERVAL * 2


def delete_game(game: Game) -> None:
if not game.is_tournament:
if not game.is_tournament and not game.is_ranked and not game.is_gauntlet:
game.delete()


Expand Down Expand Up @@ -80,93 +80,102 @@ def run_game(game_id: int) -> Optional[str]:
send_through_game_channel(game, "game.error", file_deleted.id)
raise RuntimeError("Cannot find a submission code file!")

error = 0
with black_runner as player_black, white_runner as player_white:
last_move = game.moves.create(board=INITIAL_BOARD, player="-", possible=[26, 19, 44, 37])
send_through_game_channel(game, "game.update", game_id)
exception = None

while not mod.is_game_over():
if not check_heartbeat(game) or not game.playing:
game.playing = False
game.outcome = "T"
game.forfeit = False
game.save(update_fields=["playing", "outcome", "forfeit"])
return "no ping"
board, current_player = mod.get_game_state()

try:
if current_player == Player.BLACK:
running_turn = player_black.get_move(board, current_player, black_time_limit, last_move)
elif current_player == Player.WHITE:
running_turn = player_white.get_move(board, current_player, white_time_limit, last_move)
except BaseException as e:
logger.error(f"Error when getting move {game_id}, {current_player}, {str(e)}")
task_logger.error(str(e))
exception = e

for log in running_turn:
print(log)
game_log = game.logs.create(player=current_player.value, message=log)
send_through_game_channel(game, "game.log", game_log.id)
submitted_move, error, extra_time = running_turn.return_value

if exception is not None:
error = ServerError.UNEXPECTED

if game.runoff:
if current_player == Player.BLACK:
black_time_limit = game.time_limit + extra_time
else:
white_time_limit = game.time_limit + extra_time

if error != 0:
game_err = game.errors.create(player=current_player.value, error_code=error.value[0], error_msg=error.value[1])
if isinstance(error, ServerError):
game.forfeit = False
game.outcome = "T"
elif isinstance(error, UserError):
game.forfeit = True
game.outcome = Player.BLACK.value if current_player == Player.WHITE else Player.WHITE.value
game.playing = False
game.save(update_fields=["forfeit", "outcome", "playing"])
send_through_game_channel(game, "game.error", game_err.id)
break

try:
if submitted := mod.submit_move(submitted_move):
possible = submitted
else:
game_over = True
except InvalidMoveError as e:
game_err = game.errors.create(player=current_player.value, error_code=e.code, error_msg=e.message)
game.forfeit, game.playing = True, False
game.outcome = current_player.opposite_player().value
game.save(update_fields=["forfeit", "outcome", "playing"])
send_through_game_channel(game, "game.error", game_err.id)
task_logger.info(f"{game_id}: {current_player.value} submitted invalid move {submitted}")
break

last_move = game.moves.create(
player=current_player.value,
move=submitted_move,
board=mod.get_board(),
possible=possible,
)
if game_over:
game.forfeit = False
game.outcome = mod.outcome()
game.score = mod.score()
game.save(update_fields=["forfeit", "score", "outcome", "playing"])
task_logger.info(f"GAME {game_id} OVER")
break
# print("IM HERE NOW")

try:
error = 0
with black_runner as player_black, white_runner as player_white:
last_move = game.moves.create(board=INITIAL_BOARD, player="-", possible=[26, 19, 44, 37])
send_through_game_channel(game, "game.update", game_id)
exception = None

while not mod.is_game_over():
if not check_heartbeat(game) or not game.playing:
game.playing = False
game.outcome = "T"
game.forfeit = False
game.save(update_fields=["playing", "outcome", "forfeit"])
return "no ping"
board, current_player = mod.get_game_state()
# print(board)

try:
if current_player == Player.BLACK:
running_turn = player_black.get_move(board, current_player, black_time_limit, last_move)
elif current_player == Player.WHITE:
running_turn = player_white.get_move(board, current_player, white_time_limit, last_move)
except BaseException as e:
logger.error(f"Error when getting move {game_id}, {current_player}, {str(e)}")
task_logger.error(str(e))
exception = e

for log in running_turn:
print(log)
game_log = game.logs.create(player=current_player.value, message=log)
send_through_game_channel(game, "game.log", game_log.id)
submitted_move, error, extra_time = running_turn.return_value

if exception is not None:
error = ServerError.UNEXPECTED

if game.runoff:
if current_player == Player.BLACK:
black_time_limit = game.time_limit + extra_time
else:
white_time_limit = game.time_limit + extra_time

if error != 0:
game_err = game.errors.create(player=current_player.value, error_code=error.value[0], error_msg=error.value[1])
if isinstance(error, ServerError):
game.forfeit = False
game.outcome = "T"
elif isinstance(error, UserError):
game.forfeit = True
game.outcome = Player.BLACK.value if current_player == Player.WHITE else Player.WHITE.value
game.playing = False
game.save(update_fields=["forfeit", "outcome", "playing"])
send_through_game_channel(game, "game.error", game_err.id)
break

try:
if submitted := mod.submit_move(submitted_move):
possible = submitted
else:
game_over = True
except InvalidMoveError as e:
game_err = game.errors.create(player=current_player.value, error_code=e.code, error_msg=e.message)
game.forfeit, game.playing = True, False
game.outcome = current_player.opposite_player().value
game.save(update_fields=["forfeit", "outcome", "playing"])
send_through_game_channel(game, "game.error", game_err.id)
task_logger.info(f"{game_id}: {current_player.value} submitted invalid move {submitted}")
break

last_move = game.moves.create(
player=current_player.value,
move=submitted_move,
board=mod.get_board(),
possible=possible,
)
if game_over:
game.forfeit = False
game.outcome = mod.outcome()
game.score = mod.score()
game.save(update_fields=["forfeit", "score", "outcome", "playing"])
task_logger.info(f"GAME {game_id} OVER")
break
send_through_game_channel(game, "game.update", game_id)
except BaseException as error:
print(error)

game.playing = False
game.save(update_fields=["playing"])
send_through_game_channel(game, "game.update", game_id)
black_runner.stop()
white_runner.stop()

# print("DONE", game.playing, game.score)

if error != 0 and isinstance(error, ServerError):
if error.value[0] != -8:
raise RuntimeError(f"Game {game_id} encountered a ServerError of value {error.value}")
Expand Down
2 changes: 2 additions & 0 deletions othello/apps/rating/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# things that i messed with
# import_strategy_sandboxed, __init__.py in sandboxing
15 changes: 15 additions & 0 deletions othello/apps/rating/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.contrib import admin

from .models import Gauntlet, RankedManager


class RankedManagerAdmin(admin.ModelAdmin):
readonly_fields = ("id",)


class GauntletAdmin(admin.ModelAdmin):
readonly_fields = ("id",)


admin.site.register(RankedManager, RankedManagerAdmin)
admin.site.register(Gauntlet, GauntletAdmin)
Loading