Compare commits

...

20 Commits

Author SHA1 Message Date
3046af13e5 Fixed character placement 2025-07-16 09:46:11 +01:00
ae8b0c77a5 Added text timer 2025-07-06 20:16:46 +01:00
afd1fcd6be Added comments 2025-07-06 20:16:33 +01:00
3cd2ddbd46 Changed when timer is drawn 2025-07-06 20:16:22 +01:00
f36bce66e2 Updated random timer range 2025-07-06 20:07:03 +01:00
f625c80fd3 Added captcha progress bar 2025-07-06 20:06:49 +01:00
db7faca3d9 Added red flash when wrong 2025-07-06 20:06:13 +01:00
11b1dad78b Changed order of upper() 2025-07-06 20:05:39 +01:00
3471e41272 Refined fonts 2025-07-06 20:05:14 +01:00
ae6c23b6b7 Added basic timer 2025-07-06 19:34:07 +01:00
6e70f74874 Removed all letters from Flame on Black
They all seem to be broken
2025-07-06 19:33:55 +01:00
172ffc2abf Added like two variables, literally just want to push this so I can I work on my PC 2025-07-01 18:56:22 +01:00
31b863b49b Added auto-regeneration of captcha when complete
This shit starting to look like a game lads
2025-06-30 14:43:15 +01:00
e549107fe1 Added basic implementation of Captcha class 2025-06-30 12:24:09 +01:00
220f5759e9 Removed redundant "important function" call 2025-06-27 07:53:45 +01:00
bba51ebc8c Cleaned up code 2025-06-27 07:53:02 +01:00
904601c697 Added basic implementation of createCaptcha(), Needs to be cleaned up 2025-06-27 07:18:11 +01:00
ce771d26a3 Fixed typos 2025-06-27 07:17:39 +01:00
b61a7114ba created a basic demonstraction of how text rendering should work, added some positive comments to the json opening code 2025-06-24 17:15:45 +01:00
1a34d7ca4a Added some documentation so whoever's working on this isn't entirely in the dark 2025-06-23 18:22:40 +01:00
4 changed files with 194 additions and 31 deletions

View File

@ -1,16 +1,15 @@
[ [
{"file": "Noxlock-Free.otf", "alphabet": "abcdefghijklmnopqrstuvwxyz", "dificulty": 0.0}, {"file": "Noxlock-Free.otf", "alphabet": "abcdefghijklmnopqrstuvwxyz", "difficulty": 0.0},
{"file": "Chernobyl.otf", "alphabet": "abcdefghijklmnopqrstuvwxyz0123456789", "dificulty": 0.1}, {"file": "Chernobyl.otf", "alphabet": "abcdefghijklmnopqrstuvwxyz0123456789", "difficulty": 0.1},
{"file": "VIDEO PIRATE.ttf", "alphabet": "abcdefghijklmnopqrstuvwxyz", "dificulty": 0.1}, {"file": "VIDEO PIRATE.ttf", "alphabet": "abcdefghijklmnopqrstuvwxyz", "difficulty": 0.1},
{"file": "Vampire Wars.ttf", "alphabet": "abcdefghijklmnopqrstuvwxyz0123456789", "dificulty": 0.1}, {"file": "Vampire Wars.ttf", "alphabet": "abcdefghijklmnopqrstuvwxyz0123456789", "difficulty": 0.1},
{"file": "CloisterBlack.ttf", "alphabet": "0123456789", "dificulty": 0.1}, {"file": "CloisterBlack.ttf", "alphabet": "0123456789", "difficulty": 0.1},
{"file": "Noxlock-Free.otf", "alphabet": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "dificulty": 0.2}, {"file": "Noxlock-Free.otf", "alphabet": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "difficulty": 0.2},
{"file": "CloisterBlack.ttf", "alphabet": "abcdefghijklmnopqrstuvwxyz", "dificulty": 0.3}, {"file": "CloisterBlack.ttf", "alphabet": "abcdefghijklmnopqrstuvwxyz", "difficulty": 0.5},
{"file": "LEDLIGHT.otf", "alphabet": "abcdefghijklmnopqrstuvwxyz", "dificulty": 0.4}, {"file": "LEDLIGHT.otf", "alphabet": "abcdefghijklmnopqrstuvwxyz", "difficulty": 0.4},
{"file": "Flame on Black", "alphabet": "abcdefghijklmnopqrstuvwxyz0123456789!?/#<>", "dificulty": 0.5}, {"file": "Flame on Black.ttf", "alphabet": "0123456789!?/#<>", "difficulty": 0.5},
{"file": "CloisterBlack.ttf", "alphabet": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "dificulty": 0.6}, {"file": "CloisterBlack.ttf", "alphabet": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "difficulty": 0.6},
{"file": "mevno2.ttf", "alphabet": "abcdefghijklmnopqrstuvwxyz", "dificulty": 0.8}, {"file": "CloisterBlack.ttf", "alphabet": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "difficulty": 0.8},
{"file": "CloisterBlack.ttf", "alphabet": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "dificulty": 0.8}, {"file": "RoyalInitialen.ttf", "alphabet": "abcdefghijklmnopqrstuvwxyz", "difficulty": 0.8},
{"file": "RoyalInitialen.ttf", "alphabet": "abcdefghijklmnopqrstuvwxyz", "dificulty": 0.8}, {"file": "DeathMohawk_PERSONAL_USE_ONLY.otf", "alphabet": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "difficulty": 1.0}
{"file": "DeathMohawk_PERSONAL_USE_ONLY.otf", "alphabet": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "dificulty": 1.0}
] ]

Binary file not shown.

181
game.py
View File

@ -1,12 +1,28 @@
from PIL import Image from PIL import Image, ImageDraw, ImageFont
import math as maths
import numpy as np
import logging import logging
import pygame import pygame
import gameUtils import gameUtils
import NoPELib import NoPELib
import random import random
import string
import json import json
import time
import os
# TODO: The current alphabets in the fonts.json file is kinda bullshit
# someone should probably play some test rounds and fix the diffuculty issue.
# TODO: Re-draw the captcha when a character is complete.
alphabet = string.ascii_letters + string.digits + string.punctuation
def normalDist(difficulty): def normalDist(difficulty):
"""
This function generates a random number between 0-1
that should be used to get a font with a difficulty
equal to or less than the number returned.
"""
# Create a random number between 0-1 # Create a random number between 0-1
v = random.randint(-1000, 1000) / 1000 v = random.randint(-1000, 1000) / 1000
# Redistribute the number # Redistribute the number
@ -17,6 +33,46 @@ def normalDist(difficulty):
# Cap it to be 0-1 again and return # Cap it to be 0-1 again and return
return min(max(v, 0), 1) return min(max(v, 0), 1)
def PIL2PG(pilImage: Image) -> pygame.Surface:
"""
Converts a PIL image to a Pygame surface.
"""
return pygame.image.fromstring(pilImage.tobytes(), pilImage.size, pilImage.mode)
class Font:
def __init__(self, data, fontSize):
self.path = data['file']
self.font = ImageFont.truetype(os.path.join('./fonts/', data['file']), fontSize)
self.alphabet = data['alphabet']
self.difficulty = data['difficulty']
class Captcha:
"""
Holds and manages the current captcha.
"""
def __init__(self):
self.chars = []
self.fonts = []
self.charIdx = 0 # Holds which character the player is on,
# anthing below this index should appear green.
def addChar(self, char, font):
self.chars.append(char)
self.fonts.append(font)
def match(self, char):
if self.charIdx == len(self.chars):
return True, True
matches = char.upper() == self.chars[self.charIdx].upper()
print(f'{char.upper()} == {self.chars[self.charIdx]}')
if matches:
self.charIdx += 1
return True, self.charIdx == len(self.chars)
else:
return False, self.charIdx == len(self.chars)
class Game(gameUtils.Game): class Game(gameUtils.Game):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
""" """
@ -27,27 +83,91 @@ class Game(gameUtils.Game):
self.pm (NoPELib.PlayerManager): This is where your players are stored. self.pm (NoPELib.PlayerManager): This is where your players are stored.
self.cfg (dict): Everything from the `game.toml` file. You can access it like this: self.details['title'] self.cfg (dict): Everything from the `game.toml` file. You can access it like this: self.details['title']
""" """
# Don't remove this. It does important things. :3 # Don't remove this. It does important things. :3
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# A value between 0-1 that defines which fonts the game will use # A value between 0-1 that defines which fonts the game will use
self.gameDifficulty = 0 self.gameDifficulty = 0
# The actual text in the captcha, used to check what the user typed # The actual text in the captcha, used to check what the user typed
self.currentCaptchaText = '' self.currentCaptcha = None
# The image of the captcha # The image of the captcha
self.currentCaptchaImg = '' self.currentCaptchaImg = pygame.Surface((0, 0))
# Holds when the timer started
self.fonts = {} self.timerStart = 0
# Holds how much time the user has in total (not updated)
self.timerLength = 0
# The font used for the timer
self.timerFont = pygame.font.SysFont('', 100)
# Used for animating the red flash on the current character if the users enters it wrong.
self.lastWrong = 0
self.fonts = []
self.Drawsurface = Image.new("RGB",(800,220),(255,255,255))
# I haven't finished this yet, you might want to create
# an object or something, I don't know. Keep in mind you
# have to load all of the font files for use later.
with open(f'./fonts/fonts.json', 'r') as f: with open(f'./fonts/fonts.json', 'r') as f:
j = json.loads(f.read()) for fontData in json.loads(f.read()):
self.fonts.append(Font(fontData, 100))
#self.fontList = json.loads(f.read())#loads the fonts from the json (Why the fuck was the variable called 'j' before????)
#for fontData in j: #for fontData in j:
# self.fonts.update() # self.fonts.update()
def fontsByDifficulty(self, difficulty, width=0.3):
"""
Gets a list of fonts that fall within the specified difficulty.
"""
return [
font for font in self.fonts
if font.difficulty >= difficulty-width and font.difficulty <= difficulty+width
]
def createCaptcha(self): def createCaptcha(self):
pass """
Use PIL (https://pillow.readthedocs.io/en/stable/handbook/tutorial.html)
to draw the fonts, add noise, random shapes, transforms etc. based on
self.gameDifficulty
"""
# Create a surface for that captcha
drawSurface = Image.new("RGB", (800,220), (255,255,255))
# Create a drawing object
draw = ImageDraw.Draw(drawSurface)
# Temporarily stores the current captcha text
captcha = Captcha()
# Get a random cpatcha length
capLength = random.randint(3, 6)
for i in range(capLength):
# Calculate the base X position of this character
# TODO: Make this better, I'm not too happy with my implementation here.
x = drawSurface.size[0] / capLength
x *= i
# Get the selected difficulty for this character
charDiff = normalDist(self.gameDifficulty)
# Pick a random font based on that difficulty
font = random.choice(self.fontsByDifficulty(charDiff))
# Pick a random character
char = random.choice(font.alphabet)
# Draw the selected character, with the selected font
draw.text((x, 10), char, font=font.font, fill=(0,0,0))
# Append the character to the captcha
captcha.addChar(char, font)
# Store the correct captcha text
self.currentCaptcha = captcha
# Store the created image
self.currentCaptchaImg = PIL2PG(drawSurface)
for char, font in zip(captcha.chars, captcha.fonts):
print(f'{char}: {font.path}')
# Start the timer
self.timerStart = time.perf_counter()
self.timerLength = random.randint(7, 15)
def onEvent(self, event): def onEvent(self, event):
""" """
@ -57,24 +177,51 @@ class Game(gameUtils.Game):
event (pygame.Event): The event that was fired. event (pygame.Event): The event that was fired.
""" """
if event.type == pygame.KEYDOWN: if event.type == pygame.KEYDOWN:
if event.key == pygame.K_1: if event.key == pygame.K_F1:
self.gameDifficulty -= 0.1 self.gameDifficulty -= 0.1
elif event.key == pygame.K_2: print(self.gameDifficulty)
elif event.key == pygame.K_F2:
self.gameDifficulty += 0.1 self.gameDifficulty += 0.1
elif event.key == pygame.K_3: print(self.gameDifficulty)
elif event.key == pygame.K_F3:
self.createCaptcha() self.createCaptcha()
print(self.gameDifficulty)
elif event.unicode != '' and event.unicode in alphabet:
correct, finished = self.currentCaptcha.match(event.unicode)
if not correct:
self.lastWrong = time.perf_counter()
print(correct)
if finished:
self.createCaptcha()
def update(self): def update(self):
""" """
Ran once per frame, put your drawing code and any Ran once per frame, put your drawing code and any
game logic that should be ran once per frame in here. game logic that should be ran once per frame in here.
""" """
# Don't remove this. It does important things. :3
super().update()
self.surf.fill((0, 0, 0)) r = 1 - min(time.perf_counter() - self.lastWrong, 1)
self.surf.fill((r * 255, 0, 0)) #sets the background colour
self.surf.blit(gameUtils.centre(self.currentCaptchaImg, self.size)) #draws the text to center of the screen
#TODO: actually scale the text, however, more work needs to be done on the actual function of the game first
# Draw the captcha progerss if there is a captcha
if self.currentCaptcha is not None:
x1 = self.size[0] / 2 - self.currentCaptchaImg.size[0] / 2
y = self.size[1] / 2 + self.currentCaptchaImg.size[1] / 2
x2 = x1 + (self.currentCaptcha.charIdx / len(self.currentCaptcha.chars)) * self.currentCaptchaImg.size[0]
pygame.draw.line(self.surf, (0, 255, 0), (x1, y), (x2, y), 5)
# Draw the timer if there's any time left
timeLeft = self.timerLength - (time.perf_counter() - self.timerStart)
if timeLeft > 0:
normalisedTimeLeft = max(timeLeft / self.timerLength, 0)
x = round((1-normalisedTimeLeft) * (self.size[0] // 2))
w = normalisedTimeLeft * self.size[0]
c = np.round((255, 255*normalisedTimeLeft, 255*normalisedTimeLeft))
pygame.draw.rect(self.surf, c, (x, 0, w, 10))
self.surf.blit(self.timerFont.render(str(round(timeLeft, 1)), True, c), (0, 10))
def close(self): def close(self):
""" """

17
launch.json Normal file
View File

@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger",
"type": "debugpy",
"request": "launch",
"python": "../PainJamLauncher/.venv/Scripts/python.exe",
"program": "../PainJamLauncher/__main__.py",
"args": ["captchaGame"],
"console": "integratedTerminal"
}
]
}