Starchik1 commited on
Commit
bb258a2
·
verified ·
1 Parent(s): 753f279

Upload 12 files

Browse files
app.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify, session
2
+ from flask_socketio import SocketIO, emit, join_room, leave_room
3
+ import os
4
+ import random
5
+ import json
6
+ from datetime import datetime
7
+ from flask_sqlalchemy import SQLAlchemy
8
+
9
+ app = Flask(__name__)
10
+ app.config['SECRET_KEY'] = os.urandom(24)
11
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///typing_game.db'
12
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
13
+ socketio = SocketIO(app)
14
+ db = SQLAlchemy(app)
15
+
16
+ # Модель для хранения результатов игроков
17
+ class GameResult(db.Model):
18
+ id = db.Column(db.Integer, primary_key=True)
19
+ username = db.Column(db.String(50), nullable=False)
20
+ wpm = db.Column(db.Float, nullable=False) # скорость в словах в минуту
21
+ accuracy = db.Column(db.Float, nullable=False) # точность в процентах
22
+ date = db.Column(db.DateTime, default=datetime.utcnow)
23
+
24
+ # Создание базы данных
25
+ with app.app_context():
26
+ db.create_all()
27
+
28
+ # Тексты для набора
29
+ typing_texts = [
30
+ "Быстрый набор текста - это навык, который может значительно повысить вашу продуктивность.",
31
+ "Практика ежедневного набора текста поможет вам стать быстрее и точнее.",
32
+ "Хороший наборщик текста может печатать, не глядя на клавиатуру.",
33
+ "Скорость набора измеряется в количестве слов в минуту.",
34
+ "Точность набора так же важна, как и скорость.",
35
+ "Регулярные тренировки помогут вам улучшить навыки набора текста."
36
+ ]
37
+
38
+ # Комнаты для многопользовательской игры
39
+ rooms = {}
40
+
41
+ @app.route('/')
42
+ def index():
43
+ return render_template('index.html')
44
+
45
+ @app.route('/game')
46
+ def game():
47
+ return render_template('game.html')
48
+
49
+ @app.route('/multiplayer')
50
+ def multiplayer():
51
+ return render_template('multiplayer.html')
52
+
53
+ @app.route('/leaderboard')
54
+ def leaderboard():
55
+ results = GameResult.query.order_by(GameResult.wpm.desc()).limit(10).all()
56
+ return render_template('leaderboard.html', results=results)
57
+
58
+ @app.route('/get_text', methods=['GET'])
59
+ def get_text():
60
+ text = random.choice(typing_texts)
61
+ return jsonify({'text': text})
62
+
63
+ @app.route('/save_result', methods=['POST'])
64
+ def save_result():
65
+ data = request.json
66
+ username = data.get('username', 'Anonymous')
67
+ wpm = data.get('wpm', 0)
68
+ accuracy = data.get('accuracy', 0)
69
+
70
+ result = GameResult(username=username, wpm=wpm, accuracy=accuracy)
71
+ db.session.add(result)
72
+ db.session.commit()
73
+
74
+ return jsonify({'success': True})
75
+
76
+ # Обработка WebSocket событий для многопользовательской игры
77
+ @socketio.on('join')
78
+ def on_join(data):
79
+ username = data['username']
80
+ room = data['room']
81
+ join_room(room)
82
+
83
+ if room not in rooms:
84
+ rooms[room] = {
85
+ 'players': {},
86
+ 'text': random.choice(typing_texts),
87
+ 'started': False
88
+ }
89
+
90
+ rooms[room]['players'][request.sid] = {
91
+ 'username': username,
92
+ 'progress': 0,
93
+ 'wpm': 0,
94
+ 'accuracy': 0,
95
+ 'finished': False
96
+ }
97
+
98
+ # Отправляем обновление всем в комнате
99
+ emit('room_update', rooms[room], to=room)
100
+
101
+ # Отладочное сообщение
102
+ print(f"Игрок {username} присоединился к комнате {room}")
103
+ print(f"Текущие игроки в комнате: {rooms[room]['players'].keys()}")
104
+
105
+ @socketio.on('start_game')
106
+ def on_start_game(data):
107
+ room = data['room']
108
+ if room in rooms:
109
+ rooms[room]['started'] = True
110
+ emit('game_started', {'text': rooms[room]['text']}, to=room)
111
+ print(f"Игра в комнате {room} началась. Текст: {rooms[room]['text'][:20]}...")
112
+
113
+ @socketio.on('update_progress')
114
+ def on_update_progress(data):
115
+ room = data['room']
116
+ progress = data['progress']
117
+ wpm = data['wpm']
118
+ accuracy = data['accuracy']
119
+
120
+ if room in rooms and request.sid in rooms[room]['players']:
121
+ rooms[room]['players'][request.sid]['progress'] = progress
122
+ rooms[room]['players'][request.sid]['wpm'] = wpm
123
+ rooms[room]['players'][request.sid]['accuracy'] = accuracy
124
+
125
+ if progress >= 100:
126
+ rooms[room]['players'][request.sid]['finished'] = True
127
+
128
+ emit('progress_update', rooms[room]['players'], to=room)
129
+
130
+ @socketio.on('disconnect')
131
+ def on_disconnect():
132
+ for room_id, room_data in list(rooms.items()):
133
+ if request.sid in room_data['players']:
134
+ del room_data['players'][request.sid]
135
+ leave_room(room_id)
136
+
137
+ if not room_data['players']:
138
+ del rooms[room_id]
139
+ else:
140
+ emit('room_update', room_data, to=room_id)
141
+
142
+ if __name__ == '__main__':
143
+ socketio.run(app, debug=True)
instance/typing_game.db ADDED
Binary file (8.19 kB). View file
 
requirements.txt CHANGED
@@ -1,5 +1,4 @@
1
- gunicorn
2
- flask
3
- flask-socketio
4
- python-dotenv
5
- flask-sqlalchemy
 
1
+ flask==2.0.1
2
+ flask-socketio==5.1.1
3
+ python-dotenv==0.19.0
4
+ flask-sqlalchemy==2.5.1
 
static/css/style.css ADDED
@@ -0,0 +1,542 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary-color: #1a1a2e;
3
+ --secondary-color: #16213e;
4
+ --accent-color: #0f3460;
5
+ --text-color: #e94560;
6
+ --light-text: #f8f8f8;
7
+ --error-color: #ff0000;
8
+ --success-color: #00ff00;
9
+ }
10
+
11
+ body {
12
+ font-family: 'Press Start 2P', cursive, sans-serif;
13
+ background-color: var(--primary-color);
14
+ color: var(--light-text);
15
+ margin: 0;
16
+ padding: 0;
17
+ display: flex;
18
+ flex-direction: column;
19
+ min-height: 100vh;
20
+ overflow-x: hidden;
21
+ }
22
+
23
+ .container {
24
+ width: 90%;
25
+ max-width: 1200px;
26
+ margin: 0 auto;
27
+ padding: 20px;
28
+ }
29
+
30
+ header {
31
+ background-color: var(--secondary-color);
32
+ padding: 20px 0;
33
+ text-align: center;
34
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
35
+ }
36
+
37
+ header h1 {
38
+ margin: 0;
39
+ color: var(--text-color);
40
+ font-size: 2.5rem;
41
+ text-transform: uppercase;
42
+ letter-spacing: 3px;
43
+ text-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5);
44
+ }
45
+
46
+ nav {
47
+ display: flex;
48
+ justify-content: center;
49
+ margin-top: 20px;
50
+ }
51
+
52
+ nav a {
53
+ color: var(--light-text);
54
+ text-decoration: none;
55
+ margin: 0 15px;
56
+ padding: 10px 15px;
57
+ border: 2px solid var(--accent-color);
58
+ border-radius: 5px;
59
+ transition: all 0.3s ease;
60
+ }
61
+
62
+ nav a:hover {
63
+ background-color: var(--accent-color);
64
+ transform: translateY(-3px);
65
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
66
+ }
67
+
68
+ main {
69
+ flex: 1;
70
+ padding: 40px 0;
71
+ }
72
+
73
+ .game-container {
74
+ background-color: var(--secondary-color);
75
+ border-radius: 10px;
76
+ padding: 30px;
77
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
78
+ margin-bottom: 30px;
79
+ }
80
+
81
+ .text-display {
82
+ font-size: 1.5rem;
83
+ line-height: 2;
84
+ margin-bottom: 30px;
85
+ padding: 20px;
86
+ background-color: var(--primary-color);
87
+ border-radius: 8px;
88
+ border: 2px solid var(--accent-color);
89
+ white-space: pre-wrap;
90
+ }
91
+
92
+ .text-display span {
93
+ position: relative;
94
+ }
95
+
96
+ .text-display span.correct {
97
+ color: var(--success-color);
98
+ }
99
+
100
+ .text-display span.incorrect {
101
+ color: var(--error-color);
102
+ text-decoration: underline;
103
+ }
104
+
105
+ .text-display span.current {
106
+ background-color: var(--accent-color);
107
+ }
108
+
109
+ .input-area {
110
+ width: 100%;
111
+ margin-bottom: 20px;
112
+ }
113
+
114
+ .input-area textarea {
115
+ width: 100%;
116
+ padding: 15px;
117
+ font-size: 1.2rem;
118
+ background-color: var(--primary-color);
119
+ color: var(--light-text);
120
+ border: 2px solid var(--accent-color);
121
+ border-radius: 8px;
122
+ resize: none;
123
+ font-family: 'Courier New', monospace;
124
+ }
125
+
126
+ .input-area textarea:focus {
127
+ outline: none;
128
+ border-color: var(--text-color);
129
+ box-shadow: 0 0 10px rgba(233, 69, 96, 0.5);
130
+ }
131
+
132
+ .stats {
133
+ display: flex;
134
+ justify-content: space-around;
135
+ margin-top: 20px;
136
+ padding: 15px;
137
+ background-color: var(--primary-color);
138
+ border-radius: 8px;
139
+ border: 2px solid var(--accent-color);
140
+ }
141
+
142
+ .stat-box {
143
+ text-align: center;
144
+ padding: 10px;
145
+ }
146
+
147
+ .stat-box h3 {
148
+ margin: 0;
149
+ color: var(--text-color);
150
+ font-size: 1rem;
151
+ }
152
+
153
+ .stat-box p {
154
+ font-size: 1.5rem;
155
+ margin: 10px 0 0;
156
+ }
157
+
158
+ .btn {
159
+ background-color: var(--accent-color);
160
+ color: var(--light-text);
161
+ border: none;
162
+ padding: 12px 25px;
163
+ font-size: 1rem;
164
+ border-radius: 5px;
165
+ cursor: pointer;
166
+ transition: all 0.3s ease;
167
+ font-family: 'Press Start 2P', cursive, sans-serif;
168
+ text-transform: uppercase;
169
+ letter-spacing: 1px;
170
+ }
171
+
172
+ .btn:hover {
173
+ background-color: var(--text-color);
174
+ transform: translateY(-2px);
175
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
176
+ }
177
+
178
+ .btn:active {
179
+ transform: translateY(0);
180
+ box-shadow: none;
181
+ }
182
+
183
+ .btn-primary {
184
+ background-color: var(--text-color);
185
+ }
186
+
187
+ .btn-secondary {
188
+ background-color: var(--accent-color);
189
+ }
190
+
191
+ .result-container {
192
+ text-align: center;
193
+ padding: 30px;
194
+ background-color: var(--secondary-color);
195
+ border-radius: 10px;
196
+ display: none;
197
+ }
198
+
199
+ .result-container h2 {
200
+ color: var(--text-color);
201
+ margin-bottom: 20px;
202
+ }
203
+
204
+ .result-details {
205
+ display: flex;
206
+ justify-content: space-around;
207
+ margin-bottom: 30px;
208
+ }
209
+
210
+ .result-box {
211
+ text-align: center;
212
+ padding: 15px;
213
+ background-color: var(--primary-color);
214
+ border-radius: 8px;
215
+ width: 30%;
216
+ }
217
+
218
+ .result-box h3 {
219
+ margin: 0;
220
+ color: var(--text-color);
221
+ }
222
+
223
+ .result-box p {
224
+ font-size: 2rem;
225
+ margin: 10px 0 0;
226
+ }
227
+
228
+ .overseer {
229
+ position: fixed;
230
+ top: 50%;
231
+ left: 50%;
232
+ transform: translate(-50%, -50%);
233
+ width: 300px;
234
+ height: 450px;
235
+ background-image: url('/static/images/overseer.svg');
236
+ background-size: contain;
237
+ background-repeat: no-repeat;
238
+ display: none;
239
+ z-index: 100;
240
+ filter: drop-shadow(0 0 10px rgba(255, 0, 0, 0.7));
241
+ }
242
+
243
+ .overseer.show {
244
+ display: block;
245
+ animation: overseerAppear 1.5s ease-in-out forwards;
246
+ }
247
+
248
+ @keyframes overseerAppear {
249
+ 0% {
250
+ opacity: 0;
251
+ transform: translate(-50%, -50%) scale(0.5);
252
+ }
253
+ 50% {
254
+ opacity: 1;
255
+ transform: translate(-50%, -50%) scale(1.2);
256
+ }
257
+ 70% {
258
+ transform: translate(-50%, -50%) scale(1);
259
+ }
260
+ 85% {
261
+ transform: translate(-50%, -50%) rotate(-5deg);
262
+ }
263
+ 100% {
264
+ transform: translate(-50%, -50%) rotate(0deg);
265
+ }
266
+ }
267
+
268
+ .gunshot {
269
+ position: fixed;
270
+ top: 0;
271
+ left: 0;
272
+ width: 100%;
273
+ height: 100%;
274
+ background-color: rgba(255, 0, 0, 0.7);
275
+ display: none;
276
+ z-index: 99;
277
+ animation: bloodFlash 0.5s ease-out;
278
+ }
279
+
280
+ @keyframes bloodFlash {
281
+ 0% {
282
+ opacity: 0;
283
+ }
284
+ 50% {
285
+ opacity: 1;
286
+ }
287
+ 100% {
288
+ opacity: 0;
289
+ }
290
+ }
291
+
292
+ .blood-splatter {
293
+ position: fixed;
294
+ width: 100%;
295
+ height: 100%;
296
+ top: 0;
297
+ left: 0;
298
+ z-index: 98;
299
+ pointer-events: none;
300
+ display: none;
301
+ }
302
+
303
+ .blood-splatter.show {
304
+ display: block;
305
+ }
306
+
307
+ .blood-drop {
308
+ position: absolute;
309
+ background-color: #800;
310
+ border-radius: 50%;
311
+ animation: drip 2s ease-in infinite;
312
+ }
313
+
314
+ @keyframes drip {
315
+ 0% {
316
+ transform: translateY(0) scale(1);
317
+ }
318
+ 80% {
319
+ transform: translateY(300px) scale(1.5);
320
+ opacity: 1;
321
+ }
322
+ 100% {
323
+ transform: translateY(350px) scale(1);
324
+ opacity: 0;
325
+ }
326
+ }
327
+
328
+ .muzzle-flash, .muzzle-flash-inner {
329
+ opacity: 0;
330
+ }
331
+
332
+ .overseer.firing .muzzle-flash {
333
+ opacity: 1;
334
+ animation: flash 0.3s ease-out;
335
+ }
336
+
337
+ .overseer.firing .muzzle-flash-inner {
338
+ opacity: 1;
339
+ animation: flash 0.2s ease-out;
340
+ }
341
+
342
+ @keyframes flash {
343
+ 0% {
344
+ opacity: 0;
345
+ transform: scale(0.5);
346
+ }
347
+ 50% {
348
+ opacity: 1;
349
+ transform: scale(1.5);
350
+ }
351
+ 100% {
352
+ opacity: 0;
353
+ transform: scale(0.8);
354
+ }
355
+ }
356
+
357
+ .death-message {
358
+ position: fixed;
359
+ top: 40%;
360
+ left: 50%;
361
+ transform: translate(-50%, -50%);
362
+ font-size: 3rem;
363
+ color: #ff0000;
364
+ text-shadow: 0 0 10px #000;
365
+ z-index: 101;
366
+ opacity: 0;
367
+ font-family: 'Press Start 2P', cursive;
368
+ text-transform: uppercase;
369
+ display: none;
370
+ }
371
+
372
+ .death-message.show {
373
+ display: block;
374
+ animation: messageAppear 2s ease-in-out forwards;
375
+ }
376
+
377
+ @keyframes messageAppear {
378
+ 0% {
379
+ opacity: 0;
380
+ transform: translate(-50%, -50%) scale(0.5);
381
+ }
382
+ 50% {
383
+ opacity: 1;
384
+ transform: translate(-50%, -50%) scale(1.2);
385
+ }
386
+ 100% {
387
+ opacity: 1;
388
+ transform: translate(-50%, -50%) scale(1);
389
+ }
390
+ }
391
+
392
+ .multiplayer-container {
393
+ background-color: var(--secondary-color);
394
+ border-radius: 10px;
395
+ padding: 30px;
396
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
397
+ }
398
+
399
+ .room-form {
400
+ margin-bottom: 30px;
401
+ display: flex;
402
+ flex-direction: column;
403
+ align-items: center;
404
+ }
405
+
406
+ .room-form input {
407
+ width: 100%;
408
+ max-width: 400px;
409
+ padding: 12px;
410
+ margin-bottom: 15px;
411
+ background-color: var(--primary-color);
412
+ color: var(--light-text);
413
+ border: 2px solid var(--accent-color);
414
+ border-radius: 5px;
415
+ font-family: 'Press Start 2P', cursive, sans-serif;
416
+ }
417
+
418
+ .players-list {
419
+ margin-top: 30px;
420
+ }
421
+
422
+ .player-item {
423
+ display: flex;
424
+ justify-content: space-between;
425
+ align-items: center;
426
+ padding: 15px;
427
+ margin-bottom: 10px;
428
+ background-color: var(--primary-color);
429
+ border-radius: 5px;
430
+ border-left: 5px solid var(--accent-color);
431
+ }
432
+
433
+ .player-name {
434
+ font-weight: bold;
435
+ color: var(--text-color);
436
+ }
437
+
438
+ .player-progress {
439
+ width: 60%;
440
+ height: 20px;
441
+ background-color: var(--primary-color);
442
+ border: 2px solid var(--accent-color);
443
+ border-radius: 10px;
444
+ overflow: hidden;
445
+ }
446
+
447
+ .progress-bar {
448
+ height: 100%;
449
+ background-color: var(--text-color);
450
+ width: 0%;
451
+ transition: width 0.3s ease;
452
+ }
453
+
454
+ .player-stats {
455
+ display: flex;
456
+ gap: 15px;
457
+ }
458
+
459
+ .leaderboard-container {
460
+ background-color: var(--secondary-color);
461
+ border-radius: 10px;
462
+ padding: 30px;
463
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
464
+ }
465
+
466
+ .leaderboard-table {
467
+ width: 100%;
468
+ border-collapse: collapse;
469
+ margin-top: 20px;
470
+ }
471
+
472
+ .leaderboard-table th, .leaderboard-table td {
473
+ padding: 15px;
474
+ text-align: left;
475
+ border-bottom: 2px solid var(--accent-color);
476
+ }
477
+
478
+ .leaderboard-table th {
479
+ background-color: var(--accent-color);
480
+ color: var(--light-text);
481
+ text-transform: uppercase;
482
+ }
483
+
484
+ .leaderboard-table tr:nth-child(even) {
485
+ background-color: var(--primary-color);
486
+ }
487
+
488
+ .leaderboard-table tr:hover {
489
+ background-color: var(--accent-color);
490
+ }
491
+
492
+ footer {
493
+ background-color: var(--secondary-color);
494
+ text-align: center;
495
+ padding: 20px 0;
496
+ margin-top: auto;
497
+ }
498
+
499
+ footer p {
500
+ margin: 0;
501
+ color: var(--light-text);
502
+ font-size: 0.8rem;
503
+ }
504
+
505
+ @media (max-width: 768px) {
506
+ header h1 {
507
+ font-size: 1.8rem;
508
+ }
509
+
510
+ nav {
511
+ flex-direction: column;
512
+ align-items: center;
513
+ }
514
+
515
+ nav a {
516
+ margin: 5px 0;
517
+ width: 80%;
518
+ text-align: center;
519
+ }
520
+
521
+ .text-display {
522
+ font-size: 1.2rem;
523
+ }
524
+
525
+ .stats {
526
+ flex-direction: column;
527
+ }
528
+
529
+ .stat-box {
530
+ margin-bottom: 10px;
531
+ }
532
+
533
+ .result-details {
534
+ flex-direction: column;
535
+ align-items: center;
536
+ }
537
+
538
+ .result-box {
539
+ width: 80%;
540
+ margin-bottom: 15px;
541
+ }
542
+ }
static/images/overseer.svg ADDED
static/js/game.js ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', function() {
2
+ // Элементы DOM
3
+ const textDisplay = document.getElementById('textDisplay');
4
+ const inputArea = document.getElementById('inputArea');
5
+ const wpmDisplay = document.getElementById('wpm');
6
+ const accuracyDisplay = document.getElementById('accuracy');
7
+ const timeDisplay = document.getElementById('time');
8
+ const startBtn = document.getElementById('startBtn');
9
+ const resultContainer = document.getElementById('resultContainer');
10
+ const finalWpm = document.getElementById('finalWpm');
11
+ const finalAccuracy = document.getElementById('finalAccuracy');
12
+ const finalTime = document.getElementById('finalTime');
13
+ const restartBtn = document.getElementById('restartBtn');
14
+ const overseer = document.getElementById('overseer');
15
+ const gunshot = document.getElementById('gunshot');
16
+ const usernameInput = document.getElementById('username');
17
+
18
+ // Переменные для игры
19
+ let text = '';
20
+ let startTime = null;
21
+ let endTime = null;
22
+ let timer = null;
23
+ let mistakes = 0;
24
+ let totalChars = 0;
25
+ let currentPosition = 0;
26
+ let gameActive = false;
27
+ let timeLimit = 60; // Время в секундах
28
+ let timeRemaining = timeLimit;
29
+
30
+ // Получение текста с сервера
31
+ function fetchText() {
32
+ fetch('/get_text')
33
+ .then(response => response.json())
34
+ .then(data => {
35
+ text = data.text;
36
+ displayText();
37
+ })
38
+ .catch(error => console.error('Error fetching text:', error));
39
+ }
40
+
41
+ // Отображение текста
42
+ function displayText() {
43
+ textDisplay.innerHTML = '';
44
+ for (let i = 0; i < text.length; i++) {
45
+ const span = document.createElement('span');
46
+ span.textContent = text[i];
47
+ textDisplay.appendChild(span);
48
+ }
49
+ // Выделяем первый символ как текущий
50
+ if (textDisplay.firstChild) {
51
+ textDisplay.firstChild.classList.add('current');
52
+ }
53
+ }
54
+
55
+ // Начало игры
56
+ function startGame() {
57
+ if (gameActive) return;
58
+
59
+ fetchText();
60
+ inputArea.value = '';
61
+ inputArea.disabled = false;
62
+ inputArea.focus();
63
+
64
+ mistakes = 0;
65
+ totalChars = 0;
66
+ currentPosition = 0;
67
+ timeRemaining = timeLimit;
68
+
69
+ startTime = new Date();
70
+ gameActive = true;
71
+
72
+ // Запускаем таймер
73
+ timer = setInterval(updateTimer, 1000);
74
+
75
+ // Скрываем результаты и надсмотрщика
76
+ resultContainer.style.display = 'none';
77
+ overseer.classList.remove('show');
78
+
79
+ // Обновляем статистику
80
+ updateStats();
81
+ }
82
+
83
+ // Обновление таймера
84
+ function updateTimer() {
85
+ timeRemaining--;
86
+ timeDisplay.textContent = timeRemaining;
87
+
88
+ if (timeRemaining <= 0) {
89
+ endGame(false);
90
+ }
91
+ }
92
+
93
+ // Обновление статистики
94
+ function updateStats() {
95
+ if (!gameActive) return;
96
+
97
+ const currentTime = new Date();
98
+ const timeElapsed = (currentTime - startTime) / 1000; // в секундах
99
+
100
+ // Расчет WPM (слов в минуту)
101
+ // Считаем, что среднее слово - 5 символов
102
+ const wpm = Math.round((currentPosition / 5) / (timeElapsed / 60));
103
+
104
+ // Расчет точности
105
+ const accuracy = totalChars > 0 ? Math.round(((totalChars - mistakes) / totalChars) * 100) : 100;
106
+
107
+ wpmDisplay.textContent = wpm;
108
+ accuracyDisplay.textContent = accuracy + '%';
109
+ }
110
+
111
+ // Завершение игры
112
+ function endGame(completed = true) {
113
+ clearInterval(timer);
114
+ gameActive = false;
115
+ endTime = new Date();
116
+
117
+ inputArea.disabled = true;
118
+
119
+ const timeElapsed = Math.round((endTime - startTime) / 1000);
120
+ const wpm = Math.round((currentPosition / 5) / (timeElapsed / 60));
121
+ const accuracy = totalChars > 0 ? Math.round(((totalChars - mistakes) / totalChars) * 100) : 100;
122
+
123
+ finalWpm.textContent = wpm;
124
+ finalAccuracy.textContent = accuracy + '%';
125
+ finalTime.textContent = timeElapsed + 's';
126
+
127
+ resultContainer.style.display = 'block';
128
+
129
+ // Если игра не завершена успешно или точность ниже 70%, показываем надсмотрщика
130
+ if (!completed || accuracy < 70) {
131
+ showOverseer();
132
+ }
133
+
134
+ // Сохраняем результат
135
+ if (usernameInput && usernameInput.value) {
136
+ saveResult(usernameInput.value, wpm, accuracy);
137
+ }
138
+ }
139
+
140
+ // Показ надсмотрщика и анимация выстрела
141
+ function showOverseer() {
142
+ overseer.classList.add('show');
143
+
144
+ // Создаем капли крови для разбрызгивания
145
+ createBloodSplatters();
146
+
147
+ // Через 2 секунды после появления надсмотрщика - "выстрел"
148
+ setTimeout(() => {
149
+ overseer.classList.add('firing');
150
+ gunshot.style.display = 'block';
151
+ bloodSplatter.classList.add('show');
152
+
153
+ // Показываем сообщение о смерти
154
+ deathMessage.classList.add('show');
155
+
156
+ // Звук выстрела
157
+ const audio = new Audio('/static/sounds/gunshot.mp3');
158
+ audio.play().catch(e => console.log('Audio play failed:', e));
159
+
160
+ // Эффект тряски экрана
161
+ document.body.classList.add('shake');
162
+ setTimeout(() => {
163
+ document.body.classList.remove('shake');
164
+ }, 500);
165
+
166
+ // Скрываем эффект выстрела через 500мс
167
+ setTimeout(() => {
168
+ gunshot.style.display = 'none';
169
+ }, 500);
170
+ }, 2000);
171
+ }
172
+
173
+ // Создание брызг крови
174
+ function createBloodSplatters() {
175
+ const bloodSplatter = document.getElementById('bloodSplatter');
176
+ bloodSplatter.innerHTML = '';
177
+
178
+ // Создаем 30 капель крови разного размера
179
+ for (let i = 0; i < 30; i++) {
180
+ const drop = document.createElement('div');
181
+ drop.classList.add('blood-drop');
182
+
183
+ // Случайный размер
184
+ const size = Math.random() * 20 + 5;
185
+ drop.style.width = `${size}px`;
186
+ drop.style.height = `${size}px`;
187
+
188
+ // Случайное положение
189
+ drop.style.left = `${Math.random() * 100}%`;
190
+ drop.style.top = `${Math.random() * 50}%`;
191
+
192
+ // Случайная задержка анимации
193
+ drop.style.animationDelay = `${Math.random() * 1.5}s`;
194
+
195
+ bloodSplatter.appendChild(drop);
196
+ }
197
+ }
198
+
199
+ // Сохранение результата
200
+ function saveResult(username, wpm, accuracy) {
201
+ fetch('/save_result', {
202
+ method: 'POST',
203
+ headers: {
204
+ 'Content-Type': 'application/json',
205
+ },
206
+ body: JSON.stringify({
207
+ username: username,
208
+ wpm: wpm,
209
+ accuracy: accuracy
210
+ })
211
+ })
212
+ .then(response => response.json())
213
+ .then(data => console.log('Result saved:', data))
214
+ .catch(error => console.error('Error saving result:', error));
215
+ }
216
+
217
+ // Обработка ввода
218
+ inputArea.addEventListener('input', function(e) {
219
+ if (!gameActive) return;
220
+
221
+ const inputValue = e.target.value;
222
+ const currentChar = text[currentPosition];
223
+
224
+ // Проверяем, правильно ли введен символ
225
+ if (inputValue.charAt(inputValue.length - 1) === currentChar) {
226
+ // Правильный символ
227
+ const spans = textDisplay.querySelectorAll('span');
228
+ spans[currentPosition].classList.remove('current');
229
+ spans[currentPosition].classList.add('correct');
230
+
231
+ currentPosition++;
232
+ totalChars++;
233
+
234
+ // Если есть следующий символ, делаем его текущим
235
+ if (currentPosition < text.length) {
236
+ spans[currentPosition].classList.add('current');
237
+ } else {
238
+ // Текст закончился, завершаем игру
239
+ endGame(true);
240
+ }
241
+ } else {
242
+ // Неправильный символ
243
+ mistakes++;
244
+ totalChars++;
245
+
246
+ const spans = textDisplay.querySelectorAll('span');
247
+ spans[currentPosition].classList.add('incorrect');
248
+ }
249
+
250
+ // Обновляем статистику
251
+ updateStats();
252
+ });
253
+
254
+ // Обработчики кнопок
255
+ startBtn.addEventListener('click', startGame);
256
+ restartBtn.addEventListener('click', startGame);
257
+
258
+ // Инициализация
259
+ timeDisplay.textContent = timeLimit;
260
+ });
static/js/multiplayer.js ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', function() {
2
+ // Элементы DOM
3
+ const roomForm = document.getElementById('roomForm');
4
+ const usernameInput = document.getElementById('username');
5
+ const roomInput = document.getElementById('room');
6
+ const joinBtn = document.getElementById('joinBtn');
7
+ const startBtn = document.getElementById('startBtn');
8
+ const gameContainer = document.getElementById('gameContainer');
9
+ const textDisplay = document.getElementById('textDisplay');
10
+ const inputArea = document.getElementById('inputArea');
11
+ const playersList = document.getElementById('playersList');
12
+
13
+ // Переменные для игры
14
+ let socket;
15
+ let username = '';
16
+ let room = '';
17
+ let text = '';
18
+ let startTime = null;
19
+ let currentPosition = 0;
20
+ let mistakes = 0;
21
+ let totalChars = 0;
22
+ let gameActive = false;
23
+
24
+ // Инициализация Socket.IO
25
+ function initSocket() {
26
+ socket = io();
27
+
28
+ // Обработчики событий Socket.IO
29
+ socket.on('connect', () => {
30
+ console.log('Connected to server');
31
+ });
32
+
33
+ socket.on('room_update', (data) => {
34
+ updatePlayersList(data.players);
35
+
36
+ // Если игра началась, но текст еще не отображен
37
+ if (data.started && !gameActive) {
38
+ startGame(data.text);
39
+ }
40
+
41
+ // Активируем кнопку старта только для первого игрока
42
+ const playerIds = Object.keys(data.players);
43
+ if (playerIds.length > 0 && playerIds[0] === socket.id) {
44
+ startBtn.disabled = false;
45
+ } else {
46
+ startBtn.disabled = true;
47
+ }
48
+ });
49
+
50
+ socket.on('game_started', (data) => {
51
+ startGame(data.text);
52
+ });
53
+
54
+ socket.on('progress_update', (players) => {
55
+ updatePlayersProgress(players);
56
+
57
+ // Проверяем, закончили ли все игроки
58
+ let allFinished = true;
59
+ for (const id in players) {
60
+ if (!players[id].finished) {
61
+ allFinished = false;
62
+ break;
63
+ }
64
+ }
65
+
66
+ // Если все закончили, показываем результаты
67
+ if (allFinished) {
68
+ showResults(players);
69
+ }
70
+ });
71
+
72
+ socket.on('disconnect', () => {
73
+ console.log('Disconnected from server');
74
+ });
75
+ }
76
+
77
+ // Присоединение к комнате
78
+ function joinRoom() {
79
+ username = usernameInput.value.trim();
80
+ room = roomInput.value.trim();
81
+
82
+ if (!username || !room) {
83
+ alert('Пожалуйста, введите имя пользователя и название комнаты');
84
+ return;
85
+ }
86
+
87
+ socket.emit('join', {
88
+ username: username,
89
+ room: room
90
+ });
91
+
92
+ // Скрываем форму и показываем игровой контейнер
93
+ roomForm.style.display = 'none';
94
+ gameContainer.style.display = 'block';
95
+ }
96
+
97
+ // Обновление списка игроков
98
+ function updatePlayersList(players) {
99
+ playersList.innerHTML = '';
100
+
101
+ for (const id in players) {
102
+ const player = players[id];
103
+ const playerItem = document.createElement('div');
104
+ playerItem.className = 'player-item';
105
+ playerItem.id = 'player-' + id;
106
+
107
+ const playerName = document.createElement('div');
108
+ playerName.className = 'player-name';
109
+ playerName.textContent = player.username;
110
+
111
+ const playerProgress = document.createElement('div');
112
+ playerProgress.className = 'player-progress';
113
+
114
+ const progressBar = document.createElement('div');
115
+ progressBar.className = 'progress-bar';
116
+ progressBar.style.width = player.progress + '%';
117
+
118
+ const playerStats = document.createElement('div');
119
+ playerStats.className = 'player-stats';
120
+ playerStats.innerHTML = `
121
+ <span>WPM: ${player.wpm || 0}</span>
122
+ <span>Точность: ${player.accuracy || 0}%</span>
123
+ `;
124
+
125
+ playerProgress.appendChild(progressBar);
126
+ playerItem.appendChild(playerName);
127
+ playerItem.appendChild(playerProgress);
128
+ playerItem.appendChild(playerStats);
129
+ playersList.appendChild(playerItem);
130
+ }
131
+ }
132
+
133
+ // Обновление прогресса игроков
134
+ function updatePlayersProgress(players) {
135
+ for (const id in players) {
136
+ const player = players[id];
137
+ const playerItem = document.getElementById('player-' + id);
138
+
139
+ if (playerItem) {
140
+ const progressBar = playerItem.querySelector('.progress-bar');
141
+ progressBar.style.width = player.progress + '%';
142
+
143
+ const playerStats = playerItem.querySelector('.player-stats');
144
+ playerStats.innerHTML = `
145
+ <span>WPM: ${player.wpm || 0}</span>
146
+ <span>Точность: ${player.accuracy || 0}%</span>
147
+ `;
148
+ }
149
+ }
150
+ }
151
+
152
+ // Начало игры
153
+ function startGame(textContent) {
154
+ text = textContent;
155
+ displayText();
156
+
157
+ inputArea.value = '';
158
+ inputArea.disabled = false;
159
+ inputArea.focus();
160
+
161
+ mistakes = 0;
162
+ totalChars = 0;
163
+ currentPosition = 0;
164
+
165
+ startTime = new Date();
166
+ gameActive = true;
167
+ }
168
+
169
+ // Отображение текста
170
+ function displayText() {
171
+ textDisplay.innerHTML = '';
172
+ for (let i = 0; i < text.length; i++) {
173
+ const span = document.createElement('span');
174
+ span.textContent = text[i];
175
+ textDisplay.appendChild(span);
176
+ }
177
+ // Выделяем первый символ как текущий
178
+ if (textDisplay.firstChild) {
179
+ textDisplay.firstChild.classList.add('current');
180
+ }
181
+ }
182
+
183
+ // Показ результатов
184
+ function showResults(players) {
185
+ let winner = null;
186
+ let maxWpm = 0;
187
+
188
+ for (const id in players) {
189
+ const player = players[id];
190
+ if (player.wpm > maxWpm) {
191
+ maxWpm = player.wpm;
192
+ winner = player;
193
+ }
194
+ }
195
+
196
+ if (winner) {
197
+ alert(`Игра окончена! Победитель: ${winner.username} со скоростью ${winner.wpm} WPM и точностью ${winner.accuracy}%`);
198
+ }
199
+ }
200
+
201
+ // Обработка ввода
202
+ inputArea.addEventListener('input', function(e) {
203
+ if (!gameActive) return;
204
+
205
+ const inputValue = e.target.value;
206
+ const currentChar = text[currentPosition];
207
+
208
+ // Проверяем, правильно ли введен символ
209
+ if (inputValue.charAt(inputValue.length - 1) === currentChar) {
210
+ // Правильный символ
211
+ const spans = textDisplay.querySelectorAll('span');
212
+ spans[currentPosition].classList.remove('current');
213
+ spans[currentPosition].classList.add('correct');
214
+
215
+ currentPosition++;
216
+ totalChars++;
217
+
218
+ // Если есть следующий символ, делаем его текущим
219
+ if (currentPosition < text.length) {
220
+ spans[currentPosition].classList.add('current');
221
+ } else {
222
+ // Текст закончился, завершаем игру
223
+ gameActive = false;
224
+ inputArea.disabled = true;
225
+ }
226
+ } else {
227
+ // Неправильный символ
228
+ mistakes++;
229
+ totalChars++;
230
+ }
231
+
232
+ // Обновляем статистику и отправляем на сервер
233
+ updateStats();
234
+ });
235
+
236
+ // Обновление статистики
237
+ function updateStats() {
238
+ if (!gameActive && currentPosition < text.length) return;
239
+
240
+ const currentTime = new Date();
241
+ const timeElapsed = (currentTime - startTime) / 1000; // в секундах
242
+
243
+ // Расчет WPM (слов в минуту)
244
+ // Считаем, что среднее слово - 5 символов
245
+ const wpm = Math.round((currentPosition / 5) / (timeElapsed / 60));
246
+
247
+ // Расчет точности
248
+ const accuracy = totalChars > 0 ? Math.round(((totalChars - mistakes) / totalChars) * 100) : 100;
249
+
250
+ // Расчет прогресса
251
+ const progress = Math.round((currentPosition / text.length) * 100);
252
+
253
+ // Отправляем данные на сервер
254
+ socket.emit('update_progress', {
255
+ room: room,
256
+ progress: progress,
257
+ wpm: wpm,
258
+ accuracy: accuracy
259
+ });
260
+ }
261
+
262
+ // Обработчики кнопок
263
+ joinBtn.addEventListener('click', function(e) {
264
+ e.preventDefault();
265
+ joinRoom();
266
+ });
267
+
268
+ startBtn.addEventListener('click', function() {
269
+ socket.emit('start_game', {
270
+ room: room
271
+ });
272
+ });
273
+
274
+ // Инициализация
275
+ initSocket();
276
+ });
templates/game.html ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block title %}Typing Speed Game - Одиночная игра{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="game-container">
7
+ <h2>Одиночная игра</h2>
8
+
9
+ <div class="input-area">
10
+ <input type="text" id="username" placeholder="Введите ваше имя" class="form-control">
11
+ </div>
12
+
13
+ <div class="text-display" id="textDisplay">
14
+ Нажмите "Начать игру", чтобы получить текст для набора.
15
+ </div>
16
+
17
+ <div class="input-area">
18
+ <textarea id="inputArea" placeholder="Начните вводить текст здесь..." disabled></textarea>
19
+ </div>
20
+
21
+ <div class="stats">
22
+ <div class="stat-box">
23
+ <h3>Скорость</h3>
24
+ <p id="wpm">0</p>
25
+ <span>слов/мин</span>
26
+ </div>
27
+ <div class="stat-box">
28
+ <h3>Точность</h3>
29
+ <p id="accuracy">100%</p>
30
+ </div>
31
+ <div class="stat-box">
32
+ <h3>Время</h3>
33
+ <p id="time">60</p>
34
+ <span>секунд</span>
35
+ </div>
36
+ </div>
37
+
38
+ <div class="controls">
39
+ <button id="startBtn" class="btn btn-primary">Начать игру</button>
40
+ </div>
41
+
42
+ <div class="result-container" id="resultContainer">
43
+ <h2>Результаты</h2>
44
+ <div class="result-details">
45
+ <div class="result-box">
46
+ <h3>Скорость</h3>
47
+ <p id="finalWpm">0</p>
48
+ <span>слов/мин</span>
49
+ </div>
50
+ <div class="result-box">
51
+ <h3>Точность</h3>
52
+ <p id="finalAccuracy">0%</p>
53
+ </div>
54
+ <div class="result-box">
55
+ <h3>Время</h3>
56
+ <p id="finalTime">0s</p>
57
+ </div>
58
+ </div>
59
+ <button id="restartBtn" class="btn btn-primary">Играть снова</button>
60
+ </div>
61
+
62
+ <!-- Надсмотрщик и эффект выстрела -->
63
+ <div class="overseer" id="overseer"></div>
64
+ <div class="gunshot" id="gunshot"></div>
65
+ <div class="blood-splatter" id="bloodSplatter"></div>
66
+ <div class="death-message" id="deathMessage">Вы проиграли!</div>
67
+ </div>
68
+ {% endblock %}
69
+
70
+ {% block scripts %}
71
+ <script src="{{ url_for('static', filename='js/game.js') }}"></script>
72
+ {% endblock %}
templates/index.html ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block title %}Typing Speed Game - Главная{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="game-container">
7
+ <h2>Добро пожаловать в Typing Speed Game!</h2>
8
+ <p>Проверьте свою скорость и точность набора текста в нашей захватывающей игре.</p>
9
+ <p>Но будьте осторожны! Если вы наберете текст слишком медленно или с большим количеством ошибок, надсмотрщик не будет доволен...</p>
10
+
11
+ <div class="options">
12
+ <a href="{{ url_for('game') }}" class="btn btn-primary">Начать одиночную игру</a>
13
+ <a href="{{ url_for('multiplayer') }}" class="btn btn-secondary">Играть с друзьями</a>
14
+ </div>
15
+
16
+ <div class="instructions">
17
+ <h3>Как играть:</h3>
18
+ <ol>
19
+ <li>Выберите режим игры: одиночный или мультиплеер</li>
20
+ <li>Введите свое имя</li>
21
+ <li>Набирайте текст как можно быстрее и точнее</li>
22
+ <li>Следите за своей скоростью (WPM) и точностью</li>
23
+ <li>Постарайтесь избежать встречи с надсмотрщиком!</li>
24
+ </ol>
25
+ </div>
26
+ </div>
27
+ {% endblock %}
templates/layout.html ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}Typing Speed Game{% endblock %}</title>
7
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap">
8
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
9
+ {% block extra_css %}{% endblock %}
10
+ </head>
11
+ <body>
12
+ <header>
13
+ <div class="container">
14
+ <h1>Typing Speed Game</h1>
15
+ <nav>
16
+ <a href="{{ url_for('index') }}">Главная</a>
17
+ <a href="{{ url_for('game') }}">Одиночная игра</a>
18
+ <a href="{{ url_for('multiplayer') }}">Мультиплеер</a>
19
+ <a href="{{ url_for('leaderboard') }}">Таблица лидеров</a>
20
+ </nav>
21
+ </div>
22
+ </header>
23
+
24
+ <main>
25
+ <div class="container">
26
+ {% block content %}{% endblock %}
27
+ </div>
28
+ </main>
29
+
30
+ <footer>
31
+ <div class="container">
32
+ <p>&copy; 2025 Typing Speed Game. Все права защищены.</p>
33
+ </div>
34
+ </footer>
35
+
36
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
37
+ {% block scripts %}{% endblock %}
38
+ </body>
39
+ </html>
templates/leaderboard.html ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block title %}Typing Speed Game - Таблица лидеров{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="leaderboard-container">
7
+ <h2>Таблица лидеров</h2>
8
+
9
+ <table class="leaderboard-table">
10
+ <thead>
11
+ <tr>
12
+ <th>Место</th>
13
+ <th>Имя</th>
14
+ <th>Скорость (WPM)</th>
15
+ <th>Точность</th>
16
+ <th>Дата</th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ {% for result in results %}
21
+ <tr>
22
+ <td>{{ loop.index }}</td>
23
+ <td>{{ result.username }}</td>
24
+ <td>{{ result.wpm }}</td>
25
+ <td>{{ result.accuracy }}%</td>
26
+ <td>{{ result.date.strftime('%d.%m.%Y %H:%M') }}</td>
27
+ </tr>
28
+ {% else %}
29
+ <tr>
30
+ <td colspan="5">Пока нет результатов</td>
31
+ </tr>
32
+ {% endfor %}
33
+ </tbody>
34
+ </table>
35
+ </div>
36
+ {% endblock %}
templates/multiplayer.html ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block title %}Typing Speed Game - Мультиплеер{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="multiplayer-container">
7
+ <h2>Многопользовательская игра</h2>
8
+
9
+ <form id="roomForm" class="room-form">
10
+ <input type="text" id="username" placeholder="Введите ваше имя" required>
11
+ <input type="text" id="room" placeholder="Введите название комнаты" required>
12
+ <button id="joinBtn" class="btn btn-primary">Присоединиться к комнате</button>
13
+ </form>
14
+
15
+ <div id="gameContainer" style="display: none;">
16
+ <div class="text-display" id="textDisplay">
17
+ Ожидание начала игры...
18
+ </div>
19
+
20
+ <div class="input-area">
21
+ <textarea id="inputArea" placeholder="Начните вводить текст здесь..." disabled></textarea>
22
+ </div>
23
+
24
+ <div class="controls">
25
+ <button id="startBtn" class="btn btn-primary" disabled>Начать игру</button>
26
+ </div>
27
+
28
+ <div class="players-list" id="playersList">
29
+ <!-- Список игроков будет добавлен динамически -->
30
+ </div>
31
+ </div>
32
+ </div>
33
+ {% endblock %}
34
+
35
+ {% block scripts %}
36
+ <script src="{{ url_for('static', filename='js/multiplayer.js') }}"></script>
37
+ {% endblock %}