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
22
# view страницы игрового лобби и очереди и создания игрового лобби
24
def create_lobby(request):
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
34
'title': 'Игровое лобби',
37
'max_players': GAME_MAX_PLAYERS,
40
'blanks_left': range(math.floor((GAME_MAX_PLAYERS - 1) / 2)),
41
'blanks_right': range(math.ceil((GAME_MAX_PLAYERS - 1) / 2)),
44
return render(request, 'games/lobby.html', context=context)
47
def join_lobby_ajax(request):
49
sender = AuthUser.objects.get(pk=int(request.GET.get('sender_id')))
50
lobby = Lobby.objects.get(pk=sender.current_lobby.pk)
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
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()]]
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})
66
return JsonResponse({'status': 'ok', 'url': 'http://' + request.META['HTTP_HOST'] + '/games/join_lobby/'})
68
return JsonResponse({'status': 'full'})
72
def join_lobby(request):
74
current_user = request.user
75
current_lobby = current_user.current_lobby
77
theme = True if eval(current_lobby.type)[0] == 'theme' else False
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]
83
'title': 'Игровое лобби',
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)),
92
'themes': Category.objects.all().values_list('name')
95
return render(request, 'games/lobby.html', context=context)
98
def change_game_mode(request):
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]
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})
116
return JsonResponse({'ok': 'ok'})
119
# view добавления в очередь и проверки количества игроков в ней
122
max_players = GAME_MAX_PLAYERS
123
current_user = request.user
124
current_lobby = current_user.current_lobby
126
# обработка случая обновления страницы, при котором current_lobby у пользователя убирается, а новое не создаётся
127
if current_lobby is None:
128
current_lobby = Lobby.objects.create()
129
current_user.current_lobby = current_lobby
132
# получение среднего уровня и проверка наличия существующей очереди для этого уровня
133
level = current_lobby.get_average_level // QUEUE_LEVEL_RANGE
135
new_queue = Queue.objects.filter(lowest_level=level, type=current_lobby.type).first()
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)
144
# лобби добавляется в очередь
145
current_lobby.queue = new_queue
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})
154
# если очередь заполнена, отправляется сигнал на запрос на подтверждение для всех пользователей в очереди
155
if new_queue.players_count == max_players:
156
return JsonResponse({'result': 'start', 'queue_id': new_queue.pk})
158
# если нет, отправляется сигнал ждать
159
return JsonResponse({'result': 'wait', 'queue_id': new_queue.pk})
163
def create_game(request):
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))
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
181
current_game.results[player.pk] = {'score': 0, 'answer_time': []}
183
current_queue.delete()
186
# отправляется ссылка для перехода на страницу игры
187
url = 'http://' + request.META['HTTP_HOST'] + '/games/game/'
188
return JsonResponse({'url': url})
191
# view выхода из игрового лобби
192
def quit_lobby(request):
194
current_user = request.user
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
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
219
return JsonResponse({'ok': 'ok'})
222
# view выхода из очереди
223
def cancel_queue(request):
225
# убирание очереди из queue объекта лобби
226
request.user.current_lobby.queue = None
227
request.user.current_lobby.save()
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})
237
return JsonResponse({'ok': 'ok'})
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')
251
return render(request, 'games/game.html', context)
254
# view запуска игры и игрового процесса
255
def start_game(request):
257
current_game = request.user.current_game
259
# процесс запуска игры происходит только у одного игрока (возможно стоит переделать)
260
if current_game.players[0] == str(request.user.pk):
262
# получается количество вопросов и даётся время перед началом игры
263
questions_count = GAME_QUESTIONS_COUNT
264
time.sleep(GAME_TIME_BEFORE_START)
266
# цикл обработки одного вопроса
267
for _ in range(questions_count):
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
278
# определение количества нерправильных ответов относящихся к тому же типу, но не подтипу, что и верный
279
type_answers_count = GAME_ANSWERS_COUNT - GAME_SUBTYPE_ANSWERS_COUNT - 1
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]
286
# компенсация возможного недостатка неправильных ответов того же подтипа неправильными ответами того же типа
287
if len(first_answers) != GAME_SUBTYPE_ANSWERS_COUNT:
288
type_answers_count += GAME_SUBTYPE_ANSWERS_COUNT - len(first_answers)
290
# получение необходимого количества неправильных ответов того же типа
291
type_answers = Answer.objects.filter(is_validated=True).exclude(subtype=question.answer.subtype).order_by('?')[:type_answers_count]
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)
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})
303
# даётся время на ответ
304
time.sleep(GAME_TIME_TO_ANSWER)
306
# добавление вопроса в задаваемые. объект игры достаётся из базы каждый раз заново, потому что его данные
307
# меняются и используются в других view и их надо обновлять
308
current_game = Game.objects.get(pk=request.user.current_game.pk)
309
current_game.asked_questions.add(question)
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]}
318
fastest = list(data.keys())[list(data.values()).index(min(data.values()))]
319
current_game.results[fastest]['score'] += GAME_POINTS_FOR_FASTEST
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})
328
# даётся время на просмотр верного ответа
329
time.sleep(GAME_TIME_SHOW_ANSWER)
332
current_game = Game.objects.get(pk=request.user.current_game.pk)
333
current_game.is_finished = True
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)),
342
standings = sorted([(pk, result['score']) for pk, result in current_game.results.items()],
343
key=lambda x: x[1], reverse=True)
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:]
356
# занесение мест в результаты игры
357
for i, standing in enumerate(standings):
358
current_game.results[standing[0]]['place'] = i + 1
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
367
# калибровка опыта в зависимости от уровня игрока относительно уровня игры
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)
377
# добавление опыта пользователю
378
user.current_experience += int(xp / max(int(user.level * 0.5), 1))
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
386
# убирание объекта игры из current_game всех игроков
387
user.current_game = None
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})
397
return JsonResponse({'ok': 'ok'})
400
# view проверки правильности ответа пользователя
401
def check_answer(request):
403
# определение времени ответа и его получение
404
answer_time = datetime.now()
406
current_game = Game.objects.get(pk=user.current_game.pk)
407
answer = int(request.GET.get('answer'))
410
# проверка правильности ответа, начисление баллов и добавление в results времени и правильности ответа
411
if current_game.current_question.answer.id == answer:
412
current_game.results[str(user.pk)]['score'] += GAME_POINTS_FOR_CORRECT
414
current_game.results[str(user.pk)]['answer_time'].append((result, str(answer_time)))
418
return JsonResponse({'ok': 'ok'})
421
# view страницы результатов игры
423
def results(request, game_id):
425
# получение объекта нужной игры и всех её игроков
426
current_game = Game.objects.get(pk=game_id)
427
players = AuthUser.objects.filter(pk__in=current_game.players)
429
# создание списка кортежей игрок-место-баллы
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']))
436
'title': 'Результаты игры',
437
# сортировка спика кортежей по месту
438
'results': sorted(game_results, key=lambda x: x[1])
441
return render(request, 'games/results.html', context)