Quiz

Форк
0
/
views.py 
441 строка · 21.7 Кб
1
import time
2
import random
3
import math
4
import json
5
from collections import Counter
6
from datetime import datetime
7
from django.shortcuts import render
8
from django.http import JsonResponse
9
from django.forms.models import model_to_dict
10
from django.db.models import Q
11
from django.http import HttpResponse
12
from django.template.loader import render_to_string
13
from .models import Lobby, Queue, Game, Category
14
from authapp.models import AuthUser
15
from questions.models import Question, Answer
16
from channels.layers import get_channel_layer
17
from asgiref.sync import async_to_sync
18
from variables import *
19
from django.contrib.auth.decorators import login_required
20

21

22
# view страницы игрового лобби и очереди и создания игрового лобби
23
@login_required
24
def create_lobby(request):
25

26
    # создание лобби и добавление его в объект пользователя в качестве current_lobby
27
    current_user = request.user
28
    new_lobby = Lobby.objects.create()
29
    current_user.current_lobby = new_lobby
30
    current_user.is_lobby_leader = True
31
    current_user.save()
32

33
    context = {
34
        'title': 'Игровое лобби',
35
        'user': current_user,
36
        'modes': Game.types,
37
        'max_players': GAME_MAX_PLAYERS,
38
        'users_left': [],
39
        'users_right': [],
40
        'blanks_left': range(math.floor((GAME_MAX_PLAYERS - 1) / 2)),
41
        'blanks_right': range(math.ceil((GAME_MAX_PLAYERS - 1) / 2)),
42
    }
43

44
    return render(request, 'games/lobby.html', context=context)
45

46

47
def join_lobby_ajax(request):
48

49
    sender = AuthUser.objects.get(pk=int(request.GET.get('sender_id')))
50
    lobby = Lobby.objects.get(pk=sender.current_lobby.pk)
51

52
    if lobby.players_count < GAME_MAX_PLAYERS:
53
        current_user = request.user
54
        current_user.current_lobby = lobby
55
        current_user.is_lobby_leader = False
56
        current_user.save()
57
        # friends = [x[0] for x in current_user.friends.values_list('pk') if x not in [player.pk for player in lobby.players.all()]]
58

59
        last_place = True if current_user.current_lobby.players_count == GAME_MAX_PLAYERS else False
60
        data = {'action': 'player_join', 'joiner_pk': current_user.pk, 'joiner_nickname': current_user.nickname,
61
                'last_place': last_place}
62
        layer = get_channel_layer()
63
        for user in AuthUser.objects.filter(current_lobby=current_user.current_lobby).exclude(pk=current_user.pk):
64
            async_to_sync(layer.group_send)(f'user_{user.pk}', {'type': 'send_message', 'message': data})
65

66
        return JsonResponse({'status': 'ok', 'url': 'http://' + request.META['HTTP_HOST'] + '/games/join_lobby/'})
67
    else:
68
        return JsonResponse({'status': 'full'})
69
    
70

71
@login_required
72
def join_lobby(request):
73

74
    current_user = request.user
75
    current_lobby = current_user.current_lobby
76

77
    theme = True if eval(current_lobby.type)[0] == 'theme' else False
78

79
    users_left = [j for i, j in enumerate(AuthUser.objects.filter(current_lobby=current_lobby).exclude(pk=current_user.pk)) if (i + 1) % 2 == 0]
80
    users_right = [j for i, j in enumerate(AuthUser.objects.filter(current_lobby=current_lobby).exclude(pk=current_user.pk)) if (i + 1) % 2 == 1]
81

82
    context = {
83
        'title': 'Игровое лобби',
84
        'user': current_user,
85
        'modes': Game.types,
86
        'max_players': GAME_MAX_PLAYERS,
87
        'users_left': users_left,
88
        'users_right': users_right,
89
        'blanks_left': range(math.floor((GAME_MAX_PLAYERS - 1) / 2) - len(users_left)),
90
        'blanks_right': range(math.ceil((GAME_MAX_PLAYERS - 1) / 2) - len(users_right)),
91
        'theme': theme,
92
        'themes': Category.objects.all().values_list('name')
93
    }
94

95
    return render(request, 'games/lobby.html', context=context)
96

97

98
def change_game_mode(request):
99

100
    new_type = request.GET.get('mode')
101
    lobby = Lobby.objects.get(pk=request.user.current_lobby.pk)
102
    if eval(lobby.type)[0] == 'theme':
103
        layer = get_channel_layer()
104
        for user in AuthUser.objects.filter(current_lobby=request.user.current_lobby):
105
            async_to_sync(layer.group_send)(f'user_{user.pk}',
106
                                            {'type': 'send_message', 'message': {'action': 'delete_theme'}})
107
    lobby.type = [type_ for type_ in lobby.types if type_[0] == new_type][0]
108
    lobby.save()
109

110
    if new_type == 'theme':
111
        data = {'action': 'add_theme', 'themes': list(Category.objects.all().values_list('name'))}
112
        layer = get_channel_layer()
113
        for user in AuthUser.objects.filter(current_lobby=request.user.current_lobby):
114
            async_to_sync(layer.group_send)(f'user_{user.pk}', {'type': 'send_message', 'message': data})
115

116
    return JsonResponse({'ok': 'ok'})
117

118

119
# view добавления в очередь и проверки количества игроков в ней
120
def queue(request):
121

122
    max_players = GAME_MAX_PLAYERS
123
    current_user = request.user
124
    current_lobby = current_user.current_lobby
125

126
    # обработка случая обновления страницы, при котором current_lobby у пользователя убирается, а новое не создаётся
127
    if current_lobby is None:
128
        current_lobby = Lobby.objects.create()
129
        current_user.current_lobby = current_lobby
130
        current_user.save()
131

132
    # получение среднего уровня и проверка наличия существующей очереди для этого уровня
133
    level = current_lobby.get_average_level // QUEUE_LEVEL_RANGE
134
    try:
135
        new_queue = Queue.objects.filter(lowest_level=level, type=current_lobby.type).first()
136
    except:
137
        new_queue = False
138

139
    # если подходящей очереди нет, или если в очереди нет места для всех членов лобби, создаётся новая очередь
140
    if not new_queue or new_queue.players_count + current_lobby.players_count > max_players:
141
        new_queue = Queue.objects.create(lowest_level=level, highest_level=level+QUEUE_LEVEL_RANGE,
142
                                         type=current_lobby.type)
143

144
    # лобби добавляется в очередь
145
    current_lobby.queue = new_queue
146
    current_lobby.save()
147

148
    if current_lobby.players_count > 1:
149
        data = {'action': 'queue', 'queue_id': new_queue.pk}
150
        layer = get_channel_layer()
151
        for user in AuthUser.objects.filter(current_lobby=current_lobby).exclude(pk=current_user.pk):
152
            async_to_sync(layer.group_send)(f'user_{user.pk}', {'type': 'send_message', 'message': data})
153

154
    # если очередь заполнена, отправляется сигнал на запрос на подтверждение для всех пользователей в очереди
155
    if new_queue.players_count == max_players:
156
        return JsonResponse({'result': 'start', 'queue_id': new_queue.pk})
157

158
    # если нет, отправляется сигнал ждать
159
    return JsonResponse({'result': 'wait', 'queue_id': new_queue.pk})
160

161

162
# view создания игры
163
def create_game(request):
164

165
    # получается объект очереди, а также создаётся объект игры
166
    current_queue = request.user.current_lobby.queue
167
    current_game = Game.objects.create(lowest_level=current_queue.lowest_level,
168
                                       highest_level=current_queue.highest_level,
169
                                       type=current_queue.type)
170
    if eval(current_game.type)[0] == 'theme':
171
        for theme in json.loads(request.GET['themes']):
172
            current_game.categories.add(Category.objects.get(name=theme))
173
        current_game.save()
174

175
    # объектам всех пользователей в очереди в поле current_game присваивается созданная игра,
176
    # объекты очереди и всех лобби удаляются
177
    for lobby in current_queue.lobbies.all():
178
        for player in lobby.players.all():
179
            player.current_game = current_game
180
            player.save()
181
            current_game.results[player.pk] = {'score': 0, 'answer_time': []}
182
        lobby.delete()
183
    current_queue.delete()
184
    current_game.save()
185

186
    # отправляется ссылка для перехода на страницу игры
187
    url = 'http://' + request.META['HTTP_HOST'] + '/games/game/'
188
    return JsonResponse({'url': url})
189

190

191
# view выхода из игрового лобби
192
def quit_lobby(request):
193

194
    current_user = request.user
195

196
    # если пользователь в лобби один, лобби удаляется, если нет - лобби убирается из current_lobby объекта пользователя
197
    if current_user.current_lobby is not None and current_user.current_lobby.players_count == 1:
198
        current_user.current_lobby.delete()
199
    elif current_user.current_lobby is not None:
200
        data = {'action': 'player_quit', 'quitter_pk': current_user.pk, 'quitter_nickname': current_user.nickname,
201
                'lobby_leader': False, 'u_r_alone': False}
202
        layer = get_channel_layer()
203
        users = AuthUser.objects.filter(current_lobby=request.user.current_lobby).exclude(pk=request.user.pk)
204
        if current_user.is_lobby_leader:
205
            new_leader = users.first()
206
            new_leader.is_lobby_leader = True
207
            new_leader.save()
208
            data['lobby_leader'] = True
209
            data['new_leader_pk'] = new_leader.pk
210
            data['current_mode'] = current_user.current_lobby.type[0]
211
        if current_user.current_lobby.players_count == 2:
212
            data['u_r_alone'] = True
213
        for user in AuthUser.objects.filter(current_lobby=request.user.current_lobby).exclude(pk=request.user.pk):
214
            async_to_sync(layer.group_send)(f'user_{user.pk}', {'type': 'send_message', 'message': data})
215
        current_user.current_lobby = None
216
        current_user.save()
217

218
    # ответ заглушка
219
    return JsonResponse({'ok': 'ok'})
220

221

222
# view выхода из очереди
223
def cancel_queue(request):
224

225
    # убирание очереди из queue объекта лобби
226
    request.user.current_lobby.queue = None
227
    request.user.current_lobby.save()
228

229
    # отправка сигнала для выхода из очереди всем игрокам в лобби
230
    if request.user.current_lobby.players_count > 1:
231
        data = {'action': 'cancel_queue'}
232
        layer = get_channel_layer()
233
        for user in AuthUser.objects.filter(current_lobby=request.user.current_lobby).exclude(pk=request.user.pk):
234
            async_to_sync(layer.group_send)(f'user_{user.pk}', {'type': 'send_message', 'message': data})
235

236
    # ответ заглушка
237
    return JsonResponse({'ok': 'ok'})
238

239

240
# view страницы игры
241
@login_required
242
def game(request):
243

244
    context = {
245
        'title': "Игра",
246
        # получение объектов всех игроков игры в алфавитном порядке
247
        'users': AuthUser.objects.filter(current_game=request.user.current_game).order_by('nickname'),
248
        'themes': request.user.current_game.categories.values_list('name')
249
    }
250

251
    return render(request, 'games/game.html', context)
252

253

254
# view запуска игры и игрового процесса
255
def start_game(request):
256

257
    current_game = request.user.current_game
258

259
    # процесс запуска игры происходит только у одного игрока (возможно стоит переделать)
260
    if current_game.players[0] == str(request.user.pk):
261

262
        # получается количество вопросов и даётся время перед началом игры
263
        questions_count = GAME_QUESTIONS_COUNT
264
        time.sleep(GAME_TIME_BEFORE_START)
265

266
        # цикл обработки одного вопроса
267
        for _ in range(questions_count):
268

269
            # получение случайного вопроса, которого не было в игре, и добавление его в объект игры в current_question
270
            questions = Question.objects.exclude(pk__in=current_game.asked_questions.values_list('pk')).filter(is_validated=True)
271
            current_game = Game.objects.get(pk=request.user.current_game.pk)
272
            if eval(current_game.type)[0] in ['theme', 'friend']:
273
                questions = questions.filter(category__pk__in=current_game.categories.values_list('pk'))
274
            question = questions.order_by('?').first()
275
            current_game.current_question = question
276
            current_game.save()
277

278
            # определение количества нерправильных ответов относящихся к тому же типу, но не подтипу, что и верный
279
            type_answers_count = GAME_ANSWERS_COUNT - GAME_SUBTYPE_ANSWERS_COUNT - 1
280

281
            # попытка получить нужное количество неправильных ответов того же подтипа, что и верный
282
            first_answers = Answer.objects.filter(
283
                Q(subtype=question.answer.subtype) & ~Q(pk=question.answer.pk) & Q(is_validated=True)
284
            ).order_by('?')[:GAME_SUBTYPE_ANSWERS_COUNT]
285

286
            # компенсация возможного недостатка неправильных ответов того же подтипа неправильными ответами того же типа
287
            if len(first_answers) != GAME_SUBTYPE_ANSWERS_COUNT:
288
                type_answers_count += GAME_SUBTYPE_ANSWERS_COUNT - len(first_answers)
289

290
            # получение необходимого количества неправильных ответов того же типа
291
            type_answers = Answer.objects.filter(is_validated=True).exclude(subtype=question.answer.subtype).order_by('?')[:type_answers_count]
292

293
            # преобразование всех нужных ответов в список словарей и перемешивание их
294
            answers = first_answers | Answer.objects.filter(pk=question.answer.pk, is_validated=True) | type_answers
295
            answers = list(answers.values())
296
            random.shuffle(answers)
297

298
            # отправка вопроса и ответов пользователям
299
            data = {'action': 'get_question', 'question': question.question, 'answers': answers}
300
            layer = get_channel_layer()
301
            async_to_sync(layer.group_send)(f'game_{current_game.pk}', {'type': 'send_message', 'message': data})
302

303
            # даётся время на ответ
304
            time.sleep(GAME_TIME_TO_ANSWER)
305

306
            # добавление вопроса в задаваемые. объект игры достаётся из базы каждый раз заново, потому что его данные
307
            # меняются и используются в других view и их надо обновлять
308
            current_game = Game.objects.get(pk=request.user.current_game.pk)
309
            current_game.asked_questions.add(question)
310

311
            # определение самого быстрого правильного ответа и начисление баллов за него
312
            for player, result in current_game.results.items():
313
                if len(result['answer_time']) < len(current_game.asked_questions.all()):
314
                    result['answer_time'].append((False, str(datetime.now())))
315
            data = {player: datetime.strptime(result['answer_time'][-1][1][:-7], '%Y-%m-%d %H:%M:%S') for player, result
316
                    in current_game.results.items() if result['answer_time'][-1][0]}
317
            if data:
318
                fastest = list(data.keys())[list(data.values()).index(min(data.values()))]
319
                current_game.results[fastest]['score'] += GAME_POINTS_FOR_FASTEST
320
            current_game.save()
321

322
            # отправка правильного ответа и баллов пользователям
323
            data = {'action': 'get_answer', 'correct_answer': model_to_dict(question.answer),
324
                    'score': {player: result['score'] for player, result in current_game.results.items()}}
325
            layer = get_channel_layer()
326
            async_to_sync(layer.group_send)(f'game_{current_game.pk}', {'type': 'send_message', 'message': data})
327

328
            # даётся время на просмотр верного ответа
329
            time.sleep(GAME_TIME_SHOW_ANSWER)
330

331
        # закрытие игры
332
        current_game = Game.objects.get(pk=request.user.current_game.pk)
333
        current_game.is_finished = True
334

335
        # функция получения среднего времени правильных ответов
336
        def average_time(lst):
337
            return datetime.strftime(datetime.fromtimestamp(sum(map(
338
                lambda x: datetime.timestamp(datetime.strptime(x[:-7], '%Y-%m-%d %H:%M:%S')), lst)) / len(lst)),
339
                                     '%Y-%m-%d %H:%M:%S')
340

341
        # определение мест
342
        standings = sorted([(pk, result['score']) for pk, result in current_game.results.items()],
343
                           key=lambda x: x[1], reverse=True)
344

345
        # определение мест при ничьих по очкам на основе времени правильных ответов
346
        scores = [x[1] for x in standings]
347
        scores_counter = Counter(scores)
348
        for score, count in scores_counter.items():
349
            if count > 1 and score != 0:
350
                index = scores.index(score)
351
                standings = standings[:index] + \
352
                            sorted(standings[index:index + count],
353
                                   key=lambda x: average_time([x[1] for x in current_game.results[x[0]]['answer_time'] if x[0]])) + \
354
                            standings[index + count:]
355

356
        # занесение мест в результаты игры
357
        for i, standing in enumerate(standings):
358
            current_game.results[standing[0]]['place'] = i + 1
359
        current_game.save()
360

361
        # начисление опыта за игру
362
        for pk, result in current_game.results.items():
363
            user = AuthUser.objects.get(pk=pk)
364
            xp = XP_PER_GAME / result['place']
365
            xp += XP_FIRST_PLACE_BONUS if result['place'] == 1 else 0
366

367
            # калибровка опыта в зависимости от уровня игрока относительно уровня игры
368
            xp_ratio = 1
369
            if current_game.lowest_level > user.level:
370
                xp_ratio += XP_OUT_OF_LEVEL_BONUS_RATIO \
371
                            * math.ceil((current_game.lowest_level - user.level) / QUEUE_LEVEL_RANGE)
372
            elif current_game.highest_level < user.level:
373
                xp_ratio -= XP_OUT_OF_LEVEL_BONUS_RATIO \
374
                            * math.ceil((user.level - current_game.highest_level) / QUEUE_LEVEL_RANGE)
375
            xp *= xp_ratio
376

377
            # добавление опыта пользователю
378
            user.current_experience += int(xp / max(int(user.level * 0.5), 1))
379

380
            # проверка перехода на новый уровень и его реализация
381
            if user.current_experience >= XP_PER_LEVEL:
382
                ratio = user.current_experience // XP_PER_LEVEL
383
                user.current_experience -= XP_PER_LEVEL * ratio
384
                user.level += ratio
385

386
            # убирание объекта игры из current_game всех игроков
387
            user.current_game = None
388
            user.save()
389

390
        # отправка ссылки для перехода на страницу результатов
391
        data = {'action': 'show_results',
392
                'url': f'http://{request.META["HTTP_HOST"]}/games/results/{current_game.pk}/'}
393
        layer = get_channel_layer()
394
        async_to_sync(layer.group_send)(f'game_{current_game.pk}', {'type': 'send_message', 'message': data})
395

396
    # ответ заглушка
397
    return JsonResponse({'ok': 'ok'})
398

399

400
# view проверки правильности ответа пользователя
401
def check_answer(request):
402

403
    # определение времени ответа и его получение
404
    answer_time = datetime.now()
405
    user = request.user
406
    current_game = Game.objects.get(pk=user.current_game.pk)
407
    answer = int(request.GET.get('answer'))
408
    result = False
409

410
    # проверка правильности ответа, начисление баллов и добавление в results времени и правильности ответа
411
    if current_game.current_question.answer.id == answer:
412
        current_game.results[str(user.pk)]['score'] += GAME_POINTS_FOR_CORRECT
413
        result = True
414
    current_game.results[str(user.pk)]['answer_time'].append((result, str(answer_time)))
415
    current_game.save()
416

417
    # ответ заглушка
418
    return JsonResponse({'ok': 'ok'})
419

420

421
# view страницы результатов игры
422
@login_required
423
def results(request, game_id):
424

425
    # получение объекта нужной игры и всех её игроков
426
    current_game = Game.objects.get(pk=game_id)
427
    players = AuthUser.objects.filter(pk__in=current_game.players)
428

429
    # создание списка кортежей игрок-место-баллы
430
    game_results = []
431

432
    for player in players:
433
        game_results.append((player, int(current_game.results[str(player.pk)]['place']), current_game.results[str(player.pk)]['score']))
434

435
    context = {
436
        'title': 'Результаты игры',
437
        # сортировка спика кортежей по месту
438
        'results': sorted(game_results, key=lambda x: x[1])
439
    }
440

441
    return render(request, 'games/results.html', context)
442

443

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.