-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathrun.py
424 lines (335 loc) · 15.7 KB
/
run.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
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# !/usr/bin/env python3.6 (for cloud9) #
# #
# The login system for players uses SQLAlchemy. The code has been inspired from a tutorial by #
# PrettyPrinted to suit this game environment. #
# #
# Exception handling was added to def signup() function to inform a player that a user name has already #
# been taken. # The username for the game is pulled from the SQL database and pushed to the game.html #
# #
# The glob module finds all the pathnames matching a specified pattern according to the rules used by #
# the Unix shell. #
# #
# The shutil module offers a number of high-level operations on files and collections of files. #
# In particular, # functions are provided which support file copying and removal. #
# #
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
import os
import os.path
os.path.exists('player-scores.txt')
import glob
import shutil
import json
import datetime
from shutil import copyfile
from pathlib import Path
from datetime import datetime
from flask_bootstrap import Bootstrap
from flask import Flask, redirect, render_template, request, flash, session, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from werkzeug.security import generate_password_hash, check_password_hash
from wtforms.validators import InputRequired, Length
from wtforms import Form, BooleanField, StringField, PasswordField, validators
from flask_wtf import FlaskForm
from functools import wraps
app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
Bootstrap(app)
db = SQLAlchemy(app)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# Flask LoginManager Models and FlaskForms #
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(15), unique=True, nullable=False)
password = db.Column(db.String(80), nullable=False)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
class LoginForm(FlaskForm):
username = StringField('username', validators=[
InputRequired(), Length(min=3, max=15)])
password = PasswordField('password', validators=[
InputRequired(), Length(min=8, max=80)])
remember = BooleanField('remember me')
class SignUpForm(FlaskForm):
username = StringField('username', validators=[
InputRequired(), Length(min=3, max=15)])
password = PasswordField('password', validators=[
InputRequired(), Length(min=8, max=80)])
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# App Views #
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# 1
def loginRequired(f):
@wraps(f)
def wrap(*args, **kwargs):
if 'logged_in' in session:
return f(*args, **kwargs)
else:
flash('To access this, please log in first', 'bg-danger')
return redirect(url_for('login'))
return wrap
# 2
def write_to_file(filename, data):
"""
Handel multiple function calls to write data to a text file
"""
with open(filename, "a") as file:
file.writelines(data)
# 3
def write_to_json(filename, data):
"""
Handel multiple function calls to write data to a json file
"""
with open(filename, 'w+') as json_data:
json.dump(data, json_data)
# 4
def loadRiddles():
"""
Read the riddles from the riddles txt:
"""
with open("data/riddles.json", "r") as json_data:
data = json.load(json_data)
return data
# 5
def validateAnswer(riddle, answer):
"""
check the player's answer against our own
"""
answer_given = request.form["answer"].lower()
return answer_given
# 6
def countRiddles():
"""
Count the number or riddle in out list so we can keep score! This makes our count dynamic.
"""
numRiddles = len(loadRiddles())
return numRiddles
# 7
def newUserScore(username, score):
"""
User's initial score has to be created. This is set to 0 and the score file is
created on successful login. The file is stored in a directory consisting of the
user's name, which will allow unique instances of the game, so long as the
player names are unique. This is insured but the login registration form, which
specifies unique registration names.
"""
data = {}
data['game'] = []
data['game'].append({
'date': datetime.now().strftime("%d/%m/%Y"),
'username': f'{username}',
'score': (score)
})
"""
Every instance of the game, requires a dedicated score board for the game. A pre-existing scoreboard json file is
removed at login, if it is already present. The users score always starts from 0.
"""
dir = f'data/player_data/{username}/'
if not os.path.exists(dir):
os.makedirs(dir)
else:
shutil.rmtree(dir)
os.makedirs(dir)
"""
The score board will always write over itself, permitting the score to increase.
"""
write_to_json(f'data/player_data/{username}/scores.json', data)
# 8
def writeScore(username, score):
"""
User's score has to be saved after answering each riddle. To do this, we rewrite the JSON file.
"""
data = {}
data['game'] = []
data['game'].append({
'date': datetime.now().strftime("%d/%m/%Y"),
'username': f'{username}',
'score': (score)
})
write_to_json(f'data/player_data/{username}/scores.json', data)
# 9
def loadScore(username):
"""
Read player score:
"""
with open(f'data/player_data/{username}/scores.json', 'r') as json_data:
data = json.load(json_data)
return data
# 10
def leaderborardCheck():
"""
The game requires at least 3 scores to exist, to carry out the top 3 scores
function, scores_list() at the end of the game for the first ever player.
As a work around, a scores-template.txt file is used to create the
player-scores.txt file, on first login of the first
player.
"""
import os
try:
if os.stat('data/player-scores.txt'):
os.stat('data/player-scores.txt')
except:
shutil.copyfile('data/score_template/player-scores.txt',
'data/player-scores.txt')
# only if the file doesn't exist
# 11
def write_LeaderboardScores(score, username, date):
"""
Writes all the different payer's score to player-scores.txt
"""
file = open('data/player-scores.txt', 'a')
file.write(f"\nScore: {(score)}, Player: {username}, Date: {date}")
file.close()
# 12
def scores_list():
"""
Get player-scores.txt and convert to tuples. Sort the score in each tuple
to return the top 3 scores to the leader board.
"""
li = [i.replace(",", "").replace("'", "").split()
for i in open('data/player-scores.txt').readlines()]
# picks out the score from (Score:, 10, Player:, MyName, Date:, 16/08/2018)
li.sort(key=lambda tup: tup[1])
li.sort(reverse=True) # sorts scores from highest to lowest
# Clean-up the tuple data by striping and replacing unwanted characters, for rendering to html
first = str(li[0])[1:-1].replace("'", " ").replace(",", " ")
second = str(li[1])[1:-1].replace("'", " ").replace(",", " ")
third = str(li[2])[1:-1].replace("'", " ").replace(",", " ")
fourth = str(li[3])[1:-1].replace("'", " ").replace(",", " ")
fith = str(li[4])[1:-1].replace("'", " ").replace(",", " ")
return first, second, third, fourth, fith
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# Security Conscious Views for Session, Login, Register. Logout & 404 #
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
@app.route('/')
def index():
score = 0 # our score is set to 0
if 'username' in session:
username = session['username']
newUserScore(session['username'], score) # Create a score tracker.
flash('You are logged in as ' + username)
return redirect(url_for('game', username=session['username']))
return render_template('index.html')
@app.route('/logout')
def logout():
session.clear()
flash('You are currently logged out', 'alert-success')
return redirect(url_for('index'))
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
error = None
score = 0
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user:
if check_password_hash(user.password, form.password.data):
login_user(user, remember=form.remember.data)
session['username'] = (form.username.data)
session['logged_in'] = True
# Create a leader board if one doesn't already exist.
leaderborardCheck()
# Create a score tracker.
newUserScore(form.username.data, score)
return redirect(url_for('game', username=session['username']))
flash('This is an invalid username or password', 'alert-danger')
return render_template('login.html', form=form, error=error)
@app.route('/signup', methods=['GET', 'POST'])
def signup():
"""
Register a new user. On successful registration, create user name and password in database.
create a new user score card. If a duplicate username from session is prepped for write to the
database, Exception calls a session rollback to prevent duplicate username and prompts a warning.
"""
form = SignUpForm()
error = None
score = 0
try:
if form.validate_on_submit():
hashed_password = generate_password_hash(form.password.data, method='sha256')
user = User(username=form.username.data, password=hashed_password)
db.session.add(user)
db.session.commit()
session['username'] = (form.username.data)
session['logged_in'] = True
leaderborardCheck()
newUserScore(form.username.data, score)
flash('You are now logged in', 'alert-success')
return redirect(url_for('game', username = session['username']))
except Exception:
db.session.rollback()
flash('This username already exists. Please choose again', 'alert-danger')
return render_template('signup.html', form = form, error = error)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# Game and Leader-board Views #
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
@app.route('/game/<username>', methods=["GET", "POST"])
@loginRequired
def game(username):
# riddles are stored in JSON file and are indexed
data = []
# Load JSON data from riddles.json
data = loadRiddles()
# Set the first riddle
riddleNumber = 0
# Get the current score from the scores json file
score = (loadScore(username)['game'][0]['score'])
# Get the current date for logging with the score at the end of our game
date = datetime.now().strftime("%d/%m/%Y")
if request.method == "POST":
# post riddle number x to the the the game template and
# increment the riddle by 1 each time a correct answer is
# given.
riddleNumber = int(request.form["riddleNumber"])
# Call validateAnswer function
answer_given = validateAnswer("riddle", "answer")
if data[riddleNumber]["answer"] in answer_given:
score += 1
riddleNumber += 1
# Write scores to a file that contains our username, score for each question and
# time the question was answered.
writeScore(username, score)
# Flash the number of riddles correct with the dynamic total of the
# number of riddles. Yes! The code will update for any number of riddles.
flash(
f'Well done! Thats a score of {score} out of {countRiddles()} riddles right!')
# Determines what happens next when the last riddle is used.
if riddleNumber == countRiddles():
write_LeaderboardScores(score, username, date)
return redirect(f'/leaderboard/{username}/{score}')
else:
flash(
f'Sorry {username}, \"{answer_given}\" is not the right answer... \nIt was \"{data[riddleNumber]["answer"]}\". \nLets try another.\nUse the picture clue above for help')
riddleNumber += 1
# Determines what happens next when the last riddle is used.
if riddleNumber == countRiddles():
write_LeaderboardScores(score, username, date)
return redirect(f'/leaderboard/{username}/{score}')
return render_template("game.html", username=username, riddle_me_this=data, riddleNumber=riddleNumber)
@app.route('/leaderboard/<username>/<score>')
@loginRequired
def leaderboard(username, score):
scores = score
scores = scores_list()
return render_template("leaderboard.html", username=current_user.username, player_scores=scores)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# Page not found 404 Views #
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
@app.errorhandler(404)
def page_not_found(e):
# note that we set the 404 status explicitly
return render_template('404.html'), 404
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
# IP and PORT configuration to OS Environ #
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
if __name__ == '__main__':
app.run(host=os.getenv('IP'),
port=os.getenv('PORT'),
debug=False)