-
Notifications
You must be signed in to change notification settings - Fork 83
/
level.py
443 lines (347 loc) · 14.5 KB
/
level.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
from __future__ import absolute_import, division
import json
from builtins import object, str
from datetime import datetime
from django.http import Http404, HttpResponse
from django.shortcuts import render, get_object_or_404
from django.urls import reverse
from django.utils import timezone
from django.utils.safestring import mark_safe
from django.views.decorators.http import require_POST
from rest_framework import serializers
import game.level_management as level_management
import game.messages as messages
import game.permissions as permissions
from game import app_settings
from game.cache import (
cached_default_level,
cached_episode,
cached_custom_level,
cached_level_decor,
cached_level_blocks,
)
from game.character import get_character
from game.decor import get_decor_element
from game.models import Level, Attempt, Workspace
from game.theme import get_theme
from game.views.level_solutions import solutions
from .helper import renderError
def play_custom_level_from_editor(request, levelId):
return play_custom_level(request, levelId, from_editor=True)
def play_custom_level(request, levelId, from_editor=False):
level = cached_custom_level(levelId)
if level.default:
raise Http404
return play_level(request, level, from_editor)
def play_default_level(request, levelName):
level = cached_default_level(levelName)
return play_level(request, level)
def _prev_level_url(level, user, night_mode):
"""
Find the previous available level. Check if level is available if so, go to it.
"""
if not level.prev_level.all():
return ""
prev_level = level.prev_level.all()[0]
if not user.is_anonymous and hasattr(user.userprofile, "student"):
student = user.userprofile.student
klass = student.class_field
is_prev_level_locked = klass in prev_level.locked_for_class.all()
if is_prev_level_locked:
while is_prev_level_locked and int(prev_level.name) > 1:
prev_level = prev_level.prev_level.all()[0]
is_prev_level_locked = klass in prev_level.locked_for_class.all()
return _level_url(prev_level, night_mode)
def _next_level_url(level, user, night_mode):
"""
Find the next available level. By default, this will be the `next_level` field in the Level model, but in the case
where the user is a student and the teacher has locked certain levels, then loop until we find the next unlocked
level (or we run out of levels).
"""
if not level.next_level:
return ""
next_level = level.next_level
if not user.is_anonymous and hasattr(user.userprofile, "student"):
student = user.userprofile.student
klass = student.class_field
is_next_level_locked = klass in next_level.locked_for_class.all()
if is_next_level_locked:
while is_next_level_locked and int(next_level.name) < 109:
next_level = next_level.next_level
is_next_level_locked = klass in next_level.locked_for_class.all()
return _level_url(next_level, night_mode)
def add_night(url, night_mode):
if night_mode:
return url + "?night=1"
return url
def _level_url(level, night_mode):
if level.default:
result = _default_level_url(level)
else:
result = _custom_level_url(level)
return add_night(result, night_mode)
def _default_level_url(level):
return reverse("play_default_level", args=[level.name])
def _custom_level_url(level):
return reverse("play_custom_level", args=[level.id])
def play_level(request, level, from_editor=False):
"""Loads a level for rendering in the game.
**Context**
``RequestContext``
``level``
Level that is about to be played. An instance of :model:`game.Level`.
``blocks``
Blocks that are available during the game. List of :model:`game.Block`.
``lesson``
Instruction shown at the load of the level. String from `game.messages`.
``hint``
Hint shown after a number of failed attempts. String from `game.messages`.
**Template:**
:template:`game/game.html`
"""
night_mode = False if not app_settings.NIGHT_MODE_FEATURE_ENABLED else "night" in request.GET
if not permissions.can_play_level(request.user, level, app_settings.EARLY_ACCESS_FUNCTION(request)):
return renderError(request, messages.no_permission_title(), messages.not_shared_level())
# Set default level description/hint lookups
lesson = "description_level_default"
hint = "hint_level_default"
# If it's one of our levels, set level description/hint lookups
# to point to what they should be
if level.default:
lesson = "description_level" + str(level.name)
hint = "hint_level" + str(level.name)
# Try to get the relevant message, and fall back on defaults
try:
lessonCall = getattr(messages, lesson)
hintCall = getattr(messages, hint)
except AttributeError:
lessonCall = messages.description_level_default
hintCall = messages.hint_level_default
lesson = mark_safe(lessonCall())
hint = mark_safe(hintCall())
character = level.character
character_url = character.top_down
wreckage_url = "van_wreckage.svg"
if datetime.now().month == 12:
house = get_decor_element("house", get_theme("snow")).url
cfc = get_decor_element("cfc", get_theme("snow")).url
background = get_decor_element("tile1", get_theme("snow")).url
if character == get_character("Van"):
character_url = "characters/top_view/Sleigh.svg"
wreckage_url = "sleigh_wreckage.svg"
else:
house = get_decor_element("house", level.theme).url
cfc = get_decor_element("cfc", level.theme).url
background = get_decor_element("tile1", level.theme).url
character_width = character.width
character_height = character.height
workspace = None
python_workspace = None
if not request.user.is_anonymous and hasattr(request.user.userprofile, "student"):
student = request.user.userprofile.student
attempt = (
Attempt.objects.filter(
level=level,
student=student,
finish_time__isnull=True,
night_mode=night_mode,
)
.order_by("-start_time")
.first()
)
if not attempt:
attempt = Attempt(level=level, student=student, score=None, night_mode=night_mode)
fetch_workspace_from_last_attempt(attempt)
attempt.save()
else:
attempt = close_and_reset(attempt)
workspace = attempt.workspace
python_workspace = attempt.python_workspace
decor_data = cached_level_decor(level)
if night_mode:
block_data = level_management.get_night_blocks(level)
night_mode_javascript = "true"
lesson = messages.title_night_mode()
model_solution = "[]"
else:
block_data = cached_level_blocks(level)
night_mode_javascript = "false"
model_solution = level.model_solution
return_view = "level_editor" if from_editor else "levels"
temp_block_data = []
[temp_block_data.append(block) for block in block_data if block not in temp_block_data]
block_data = temp_block_data
return render(
request,
"game/game.html",
context={
"level": level,
"lesson": lesson,
"blocks": block_data,
"decor": decor_data,
"character": character,
"background": background,
"house": house,
"cfc": cfc,
"hint": hint,
"workspace": workspace,
"python_workspace": python_workspace,
"return_url": reverse(return_view),
"character_url": character_url,
"character_width": character_width,
"character_height": character_height,
"wreckage_url": wreckage_url,
"night_mode": night_mode_javascript,
"night_mode_feature_enabled": str(app_settings.NIGHT_MODE_FEATURE_ENABLED).lower(),
"model_solution": model_solution,
"prev_level_url": _prev_level_url(level, request.user, night_mode),
"next_level_url": _next_level_url(level, request.user, night_mode),
"flip_night_mode_url": _level_url(level, not night_mode),
},
)
def fetch_workspace_from_last_attempt(attempt):
latest_attempt = (
Attempt.objects.filter(level=attempt.level, student=attempt.student, night_mode=attempt.night_mode)
.order_by("-start_time")
.first()
)
if latest_attempt:
attempt.workspace = latest_attempt.workspace
attempt.python_workspace = latest_attempt.python_workspace
def delete_level(request, levelID):
success = False
level = Level.objects.get(id=levelID)
if permissions.can_delete_level(request.user, level):
level_management.delete_level(level)
success = True
return HttpResponse(json.dumps({"success": success}), content_type="application/javascript")
def submit_attempt(request):
"""Processes a request on submission of the program solving the current level."""
if not request.user.is_anonymous and request.method == "POST" and hasattr(request.user.userprofile, "student"):
level = get_object_or_404(Level, id=request.POST.get("level", 1))
student = request.user.userprofile.student
attempt = Attempt.objects.filter(level=level, student=student, finish_time__isnull=True).first()
if attempt:
attempt.score = float(request.POST.get("score"))
attempt.workspace = request.POST.get("workspace")
attempt.python_workspace = request.POST.get("python_workspace")
record_best_attempt(attempt)
close_and_reset(attempt)
return HttpResponse("[]", content_type="application/json")
def record_best_attempt(attempt):
best_attempt = Attempt.objects.filter(
level=attempt.level,
student=attempt.student,
night_mode=attempt.night_mode,
is_best_attempt=True,
).first()
if best_attempt and (best_attempt.score <= attempt.score):
best_attempt.is_best_attempt = False
best_attempt.save()
attempt.is_best_attempt = True
elif not best_attempt:
attempt.is_best_attempt = True
def close_and_reset(attempt):
attempt.finish_time = timezone.now()
attempt.save()
new_attempt = Attempt(
level=attempt.level,
student=attempt.student,
score=None,
night_mode=attempt.night_mode,
workspace=attempt.workspace,
python_workspace=attempt.python_workspace,
)
new_attempt.save()
return new_attempt
def load_list_of_workspaces(request):
workspaces_owned = []
if permissions.can_create_workspace(request.user):
workspaces_owned = Workspace.objects.filter(owner=request.user.userprofile)
workspaces = [
{
"id": workspace.id,
"name": workspace.name,
"blockly_enabled": workspace.blockly_enabled,
"python_enabled": workspace.python_enabled,
}
for workspace in workspaces_owned
]
return HttpResponse(json.dumps(workspaces), content_type="application/json")
def load_workspace(request, workspaceID):
workspace = Workspace.objects.get(id=workspaceID)
if permissions.can_load_workspace(request.user, workspace):
return HttpResponse(
json.dumps(
{
"contents": workspace.contents,
"python_contents": workspace.python_contents,
}
),
content_type="application/json",
)
return HttpResponse(json.dumps(""), content_type="application/json")
def save_workspace(request, workspaceID=None):
request_params = ["name", "contents", "python_contents", "blockly_enabled", "python_enabled", "pythonViewEnabled"]
missing_params = [param for param in request_params if param not in request.POST]
if missing_params != []:
raise Exception("Request missing the following required parameters", missing_params)
name = request.POST.get("name")
contents = request.POST.get("contents")
python_contents = request.POST.get("python_contents")
blockly_enabled = json.loads(request.POST.get("blockly_enabled"))
python_enabled = json.loads(request.POST.get("python_enabled"))
python_view_enabled = json.loads(request.POST.get("pythonViewEnabled"))
workspace = None
if workspaceID:
workspace = Workspace.objects.get(id=workspaceID)
elif permissions.can_create_workspace(request.user):
workspace = Workspace(owner=request.user.userprofile)
if workspace and permissions.can_save_workspace(request.user, workspace):
workspace.name = name
workspace.contents = contents
workspace.python_contents = python_contents
workspace.blockly_enabled = blockly_enabled
workspace.python_enabled = python_enabled
workspace.python_view_enabled = python_view_enabled
workspace.save()
return load_list_of_workspaces(request)
def load_workspace_solution(request, level_name):
if level_name in solutions:
workspace = Workspace(owner=request.user.userprofile)
workspace.id = level_name
workspace.name = level_name
workspace.contents = solutions["blockly_default"]
workspace.python_contents = solutions["python_default"]
level = Level.objects.get(name=level_name, default=True)
if level.blocklyEnabled:
workspace.contents = solutions[level_name]
workspace.blockly_enabled = True
workspace.python_enabled = False
else:
workspace.python_contents = solutions[level_name]
workspace.blockly_enabled = False
workspace.python_enabled = True
return HttpResponse(
json.dumps(
{
"contents": workspace.contents,
"python_contents": workspace.python_contents,
}
),
content_type="application/json",
)
raise Http404
def start_episode(request, episodeId):
episode = cached_episode(episodeId)
return play_level(request, episode.first_level, False)
@require_POST
def delete_workspace(request, workspaceID):
workspace = Workspace.objects.get(id=workspaceID)
if permissions.can_delete_workspace(request.user, workspace):
workspace.delete()
return load_list_of_workspaces(request)
class LevelSerializer(serializers.ModelSerializer):
class Meta(object):
model = Level
fields = "__all__"