Compare commits
10 Commits
da8ab4e448
...
4575584b9d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4575584b9d | ||
|
|
510ea220cc | ||
|
|
0754f82ab6 | ||
|
|
b9715a8032 | ||
|
|
61ec23b3a4 | ||
|
|
4c5e89aff7 | ||
|
|
4d1399ec9b | ||
|
|
085f5dab57 | ||
|
|
05f06c8bf9 | ||
|
|
4ee3d12933 |
13
README.md
13
README.md
@ -8,6 +8,19 @@ Simple homework printer using a thermal printer. Practice makes perfect!
|
||||
pip install python-escpos[all] --user
|
||||
```
|
||||
|
||||
### Chess module
|
||||
|
||||
```
|
||||
pip install chess
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
|
||||
Consult python-escpos docs:
|
||||
- [Usage](https://python-escpos.readthedocs.io/en/latest/user/usage.html)
|
||||
- [Available profiles](https://python-escpos.readthedocs.io/en/latest/printer_profiles/available-profiles.html)
|
||||
|
||||
## Run
|
||||
|
||||
```shell
|
||||
|
||||
@ -4,6 +4,9 @@ class Job:
|
||||
def get_name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def configure(self):
|
||||
pass
|
||||
|
||||
def print_body(self, p):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import random
|
||||
try:
|
||||
import chess
|
||||
except ImportError:
|
||||
chess = None
|
||||
from .base import Job
|
||||
|
||||
class ChessPuzzleJob(Job):
|
||||
@ -6,32 +10,12 @@ class ChessPuzzleJob(Job):
|
||||
return "SACHOVE ULOHY"
|
||||
|
||||
def print_body(self, p):
|
||||
# (FEN, Instruction, Solution)
|
||||
puzzles = [
|
||||
("r1bqkb1r/pppp1ppp/2n2n2/4p2Q/2B1P3/8/PPPP1PPP/RNB1K1NR w KQkq - 4 4", "Bily na tahu. Mat 1. tahem.", "Qxf7# (Scholar's Mate)"),
|
||||
("rnb1kbnr/pppp1ppp/8/4p3/6Pq/5P2/PPPPP2P/RNBQKBNR b KQkq - 1 3", "Cerny na tahu. Mat 1. tahem.", "Qh4# (Fool's Mate)"),
|
||||
("6k1/5ppp/8/8/8/8/5PPP/4R1K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Re8# (Back Rank Mate)"),
|
||||
("3r2k1/p4ppp/1p6/8/8/8/P4PPP/3R2K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Rxd8#"),
|
||||
("6rk/5p1p/3N4/8/8/8/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Nf7# (Smothered Mate)"),
|
||||
("8/4N1pk/8/7R/8/8/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Rh5# (Anastasia's Mate)"),
|
||||
("7k/R7/5N2/8/8/8/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Rh7# (Arabian Mate)"),
|
||||
("2kr4/1pp5/B7/8/8/8/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Ba6# (Boden's Mate)"),
|
||||
("7k/6pp/6P1/7Q/8/8/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Qh7#"),
|
||||
("6k1/5p1p/5PpQ/8/8/8/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Qg7# (Lolli's Mate)"),
|
||||
("3k4/5ppp/8/6B1/8/8/5PPP/3R2K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Rd8# (Opera Mate)"),
|
||||
("6k1/5P2/8/8/8/8/5PPP/6KR w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Rh8# (Anderssen's Mate)"),
|
||||
("7k/5ppp/8/7Q/8/2B5/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Qxh7# (Damiano's Bishop Mate)"),
|
||||
("4k3/8/4K3/8/8/8/8/5Q2 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Qf7# (Dovetail Mate)"),
|
||||
("2rkr3/8/2Q5/8/8/8/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Qd7# (Swallow's Tail Mate)"),
|
||||
("3rkr2/8/4Q3/8/8/8/5PPP/6K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Qe6# (Epaulette Mate)"),
|
||||
("7k/6p1/8/8/2B5/8/5PPP/R5K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Rh1# (Greco's Mate)"),
|
||||
("6k1/5pP1/5N2/8/8/8/5PPP/3R2K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Rd8# (Hook Mate)"),
|
||||
("8/8/8/8/8/5K2/6Q1/7k w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Qg2#"),
|
||||
("k7/1R6/8/8/8/8/8/R5K1 w - - 0 1", "Bily na tahu. Mat 1. tahem.", "Ra8# (Ladder Mate)")
|
||||
]
|
||||
if chess is None:
|
||||
p.text("CHYBA: Neni nainstalovana knihovna 'chess'.\n")
|
||||
p.text("Spust: pip install chess\n\n")
|
||||
return
|
||||
|
||||
puzzle = random.choice(puzzles)
|
||||
fen, instruction, solution = puzzle
|
||||
fen, instruction, solution = self.generate_puzzle()
|
||||
|
||||
p.text(f"{instruction}\n\n")
|
||||
self.print_board(p, fen)
|
||||
@ -44,6 +28,37 @@ class ChessPuzzleJob(Job):
|
||||
p.text("Reseni (naskenuj):\n")
|
||||
p.qr(solution, size=6, native=True)
|
||||
|
||||
def generate_puzzle(self):
|
||||
"""
|
||||
Simulates a random game until a 'Mate in 1' situation arises.
|
||||
"""
|
||||
while True:
|
||||
board = chess.Board()
|
||||
# Play up to 120 ply (60 moves)
|
||||
for _ in range(120):
|
||||
if board.is_game_over():
|
||||
break
|
||||
|
||||
legal_moves = list(board.legal_moves)
|
||||
random.shuffle(legal_moves)
|
||||
|
||||
# Check if any move leads to immediate checkmate
|
||||
for move in legal_moves:
|
||||
board.push(move)
|
||||
if board.is_checkmate():
|
||||
# Found a puzzle!
|
||||
board.pop() # Revert to state before mate
|
||||
solution = board.san(move)
|
||||
turn = "Bily" if board.turn == chess.WHITE else "Cerny"
|
||||
return board.fen(), f"{turn} na tahu. Mat jednim tahem.", solution
|
||||
board.pop()
|
||||
|
||||
# No mate found, play a random move to progress the game
|
||||
if legal_moves:
|
||||
board.push(random.choice(legal_moves))
|
||||
else:
|
||||
break
|
||||
|
||||
def print_board(self, p, fen):
|
||||
board_str = fen.split(' ')[0]
|
||||
rows = board_str.split('/')
|
||||
|
||||
33
jobs/czech_words.py
Normal file
33
jobs/czech_words.py
Normal file
@ -0,0 +1,33 @@
|
||||
CZECH_WORDS = [
|
||||
"AUTO", "BABIČKA", "BANÁN", "BARVA", "BÁSNIČKA", "BATOH", "BAVLNA", "BAZÉN",
|
||||
"BEDNA", "BERAN", "BLÁTO", "BLESK", "BLOUDIT", "BOBŘÍK", "BODLÁK", "BOTY",
|
||||
"BOUDA", "BOUŘKA", "BRÁNA", "BRATR", "BROUK", "BRUSLE", "BUBEN", "BUNDA",
|
||||
"CESTA", "CIHLA", "CIRKUS", "CITRON", "CUKR", "ČEPICE", "ČERVENÁ", "ČESNEK",
|
||||
"ČÍSLO", "ČLOVĚK", "DÁREK", "DATEL", "DCERA", "DĚDEČEK", "DEŠTNÍK", "DIVADLO",
|
||||
"DOKTOR", "DOMOV", "DOPIS", "DORT", "DRAK", "DUBEN", "DŮM", "DVEŘE",
|
||||
"DŽBÁN", "FARA", "FIALA", "FILM", "FLÉTNA", "FOTBAL", "GUMA", "HAVRAN",
|
||||
"HLAVA", "HLÍNA", "HNÍZDO", "HODINY", "HOLKA", "HOLUB", "HORA", "HOUSKA",
|
||||
"HRAD", "HRÁBĚ", "HRANA", "HRAČKA", "HRNEK", "HRUŠKA", "HUDBA", "CHATA",
|
||||
"CHLÉB", "CHLAP", "CHYBA", "JABLKO", "JAHODA", "JARO", "JAZYK", "JEHLA",
|
||||
"JEZERO", "JÍDLO", "JITRO", "KABÁT", "KAČNA", "KAKAO", "KÁMEN", "KAPSÁŘ",
|
||||
"KARTA", "KÁVA", "KILO", "KLADIVO", "KLÍČ", "KLOBOUK", "KLUK", "KNIHA",
|
||||
"KOČKA", "KOLO", "KOMÁR", "KONEC", "KOPEC", "KOŘEN", "KOST", "KOŠILE",
|
||||
"KOZA", "KRÁL", "KRÁVA", "KRESLIT", "KREV", "KRK", "KRUH", "KUCHYNĚ",
|
||||
"KVĚTINA", "LÁSKA", "LAVICE", "LEDEN", "LES", "LÉTO", "LEV", "LÍSTEK",
|
||||
"LOĎ", "LOUKA", "LŽÍCE", "MÁMA", "MAPA", "MASO", "MĚSTO", "METR",
|
||||
"MLÉKO", "MLÝN", "MODRÁ", "MOŘE", "MOST", "MOTÝL", "MRAK", "MRKEV",
|
||||
"MUŽ", "MYŠ", "NÁDRAŽÍ", "NOHA", "NOC", "NOS", "NOVINY", "NŮŽKY",
|
||||
"OBCHOD", "OBĚD", "OBRAZ", "OCAS", "OKNO", "OKO", "OLEJ", "OPICE",
|
||||
"OREL", "OVOCE", "PÁTEK", "PES", "PÍSEŇ", "PIVO", "PLÁČ", "PLOT",
|
||||
"POLE", "POLÉVKA", "POMOC", "POSTEL", "POTOK", "PRÁCE", "PRAHA", "PRASE",
|
||||
"PRST", "PTÁK", "RÁDIO", "RADOST", "RAKETA", "RÁNO", "RUKA", "RYBA",
|
||||
"ŘEKA", "ŘEPA", "SÁŇKY", "SEDLO", "SESTRA", "SEŠIT", "SKLO", "SLON",
|
||||
"SLUNCE", "SNÍH", "SOVA", "SRDCE", "STROM", "STŮL", "SVĚTLO", "ŠKOLA",
|
||||
"ŠATY", "ŠNEK", "TÁTA", "TELEVIZE", "TETA", "TMA", "TRAVÁ", "TRH",
|
||||
"TUŽKA", "UCHO", "ULICE", "ÚNOR", "ÚSTA", "VAJÍČKO", "VÁNOCE", "VČELA",
|
||||
"VEČEŘE", "VEJCE", "VELRYBA", "VESNICE", "VĚTR", "VODA", "VOJÁK", "VOLANT",
|
||||
"VRÁNA", "VRATA", "VLASY", "VLAK", "VLK", "ZÁHADA", "ZAHRADA", "ZÁMEK",
|
||||
"ZIMA", "ZLATO", "ZUB", "ZVON", "ŽÁBA", "ŽÁROVKA", "ŽENA", "ŽIDLE", "ŽIVOT",
|
||||
# special letters to make sure we have them covered:
|
||||
"WEB", "PIXEL", "TEQUILA", "BRAWL", "BOX", "QÍK"
|
||||
]
|
||||
31
jobs/decimal_division.py
Normal file
31
jobs/decimal_division.py
Normal file
@ -0,0 +1,31 @@
|
||||
import random
|
||||
from .base import Job
|
||||
|
||||
class DecimalDivisionJob(Job):
|
||||
def get_name(self):
|
||||
return "DELENI (2 DES. MISTA)"
|
||||
|
||||
def print_body(self, p):
|
||||
p.text("Vypocitej na 2 desetinna mista:\n\n")
|
||||
|
||||
results = []
|
||||
|
||||
exercises = 2;
|
||||
for i in range(1, exercises + 1):
|
||||
# Ensure it's not too easy (avoid 1)
|
||||
divisor = random.randint(2, 39)
|
||||
# Dividend between 10 and 100
|
||||
dividend = random.randint(10, 1000)
|
||||
|
||||
p.text(f"{i}) {dividend} : {divisor} = ______\n\n")
|
||||
|
||||
# Calculate result rounded to 2 decimal places
|
||||
res = dividend / divisor
|
||||
results.append(f"{i}) {res:.2f}")
|
||||
|
||||
p.text("\n\n\n\n") # add space for calculations
|
||||
|
||||
p.text("Reseni (naskenuj):\n")
|
||||
# Join results for the QR code
|
||||
qr_data = "\n".join(results)
|
||||
p.qr(qr_data, size=6, native=True)
|
||||
62
jobs/division_cipher.py
Normal file
62
jobs/division_cipher.py
Normal file
@ -0,0 +1,62 @@
|
||||
import random
|
||||
import unicodedata
|
||||
from .base import Job
|
||||
from jobs.czech_words import CZECH_WORDS
|
||||
|
||||
class DivisionCipherJob(Job):
|
||||
def __init__(self):
|
||||
self.secret = "TAJENKA"
|
||||
|
||||
def get_name(self):
|
||||
return "TAJENKA DELENIM"
|
||||
|
||||
def configure(self):
|
||||
print("\n--- Configure Division Cipher ---")
|
||||
phrase = input("Enter secret phrase (default: Random): ").strip().upper()
|
||||
|
||||
raw_secret = phrase if phrase else random.choice(CZECH_WORDS)
|
||||
|
||||
# Remove accents to ensure mapping to A-Z works
|
||||
nfkd_form = unicodedata.normalize('NFKD', raw_secret)
|
||||
only_ascii = "".join([c for c in nfkd_form if not unicodedata.combining(c)])
|
||||
|
||||
# Keep only A-Z
|
||||
self.secret = "".join([c for c in only_ascii.upper() if 'A' <= c <= 'Z'])
|
||||
|
||||
if not self.secret:
|
||||
self.secret = "TAJENKA"
|
||||
|
||||
def print_body(self, p):
|
||||
secret = self.secret
|
||||
|
||||
p.text("Vylusti tajenku!\n")
|
||||
p.text("Vysledek deleni je poradi pismena\n")
|
||||
p.text("v abecede (A=1, B=2, C=3...).\n\n")
|
||||
|
||||
problems = []
|
||||
for char in secret:
|
||||
# A=1, B=2...
|
||||
target = ord(char) - ord('A') + 1
|
||||
|
||||
# Generate Dividend / Divisor = target
|
||||
# Ensure divisor is small enough for mental math
|
||||
divisor = random.randint(2, 9)
|
||||
dividend = target * divisor
|
||||
|
||||
problems.append((dividend, divisor))
|
||||
|
||||
# Print problems
|
||||
for i, (dividend, divisor) in enumerate(problems):
|
||||
p.text(f"{i+1}) {dividend} : {divisor} = ___\n")
|
||||
|
||||
p.text("\n")
|
||||
|
||||
# Print slots for solution
|
||||
p.text("Tajenka: " + " ".join(["___"] * len(secret)) + "\n\n")
|
||||
|
||||
# Print helper key
|
||||
p.text("Napoveda:\n")
|
||||
p.text("1=A 2=B 3=C 4=D 5=E 6=F 7=G 8=H\n")
|
||||
p.text("9=I 10=J 11=K 12=L 13=M 14=N 15=O\n")
|
||||
p.text("16=P 17=Q 18=R 19=S 20=T 21=U 22=V\n")
|
||||
p.text("23=W 24=X 25=Y 26=Z\n")
|
||||
12
jobs/flush.py
Normal file
12
jobs/flush.py
Normal file
@ -0,0 +1,12 @@
|
||||
class FlushJob:
|
||||
def get_name(self):
|
||||
return "Flush Printer Queue"
|
||||
|
||||
def configure(self):
|
||||
pass
|
||||
|
||||
def run(self, printer):
|
||||
# Send NUL bytes to push any buffered data without printing visible characters
|
||||
printer._raw(b'\x00\x00')
|
||||
# Send a newline to ensure any line-buffered data is processed
|
||||
printer.text("\n")
|
||||
47
jobs/joke.py
Normal file
47
jobs/joke.py
Normal file
@ -0,0 +1,47 @@
|
||||
import textwrap
|
||||
from jobs.joke_sources import JednorozecJokeSource, BestPageJokeSource
|
||||
|
||||
class JokeJob:
|
||||
def __init__(self):
|
||||
self.sources = [
|
||||
JednorozecJokeSource(),
|
||||
BestPageJokeSource()
|
||||
]
|
||||
self.selected_source = self.sources[0]
|
||||
|
||||
def get_name(self):
|
||||
return "Random Joke"
|
||||
|
||||
def configure(self):
|
||||
print("\nSelect Joke Source:")
|
||||
for i, source in enumerate(self.sources):
|
||||
print(f" [{i + 1}] {source.get_name()}")
|
||||
|
||||
choice = input(f"Choice [{self.sources.index(self.selected_source) + 1}]: ").strip()
|
||||
if choice:
|
||||
try:
|
||||
idx = int(choice) - 1
|
||||
if 0 <= idx < len(self.sources):
|
||||
self.selected_source = self.sources[idx]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def run(self, printer):
|
||||
try:
|
||||
joke = self.selected_source.fetch_joke()
|
||||
|
||||
if joke:
|
||||
# Wrap text to avoid word splitting (assuming ~42 chars for 80mm paper)
|
||||
wrapped_joke = "\n".join([textwrap.fill(line, width=42) for line in joke.splitlines()])
|
||||
|
||||
printer.text(f"Joke from {self.selected_source.get_name()}:\n")
|
||||
printer.text("--------------------------------\n\n")
|
||||
printer.text(wrapped_joke)
|
||||
printer.text("\n\n")
|
||||
else:
|
||||
printer.text("Sorry, could not extract any jokes from the website.\n")
|
||||
|
||||
except Exception as e:
|
||||
printer.text(f"Error fetching joke: {e}\n")
|
||||
|
||||
printer.cut()
|
||||
97
jobs/joke_sources.py
Normal file
97
jobs/joke_sources.py
Normal file
@ -0,0 +1,97 @@
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import random
|
||||
|
||||
class JokeSource:
|
||||
def get_name(self):
|
||||
return "Generic Source"
|
||||
|
||||
def fetch_joke(self):
|
||||
"""Returns a single joke string or None."""
|
||||
raise NotImplementedError
|
||||
|
||||
class JednorozecJokeSource(JokeSource):
|
||||
def get_name(self):
|
||||
return "vtipy.jednorozec.cz"
|
||||
|
||||
def fetch_joke(self):
|
||||
url = "https://vtipy.jednorozec.cz/"
|
||||
try:
|
||||
# Add a User-Agent to be polite and avoid basic blocking
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (compatible; PrintServer/1.0)'}
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
jokes = []
|
||||
|
||||
# Strategy 1: Look for specific classes often used in blogs/joke sites
|
||||
# We look for divs that might contain the joke text
|
||||
potential_classes = ['post', 'entry', 'hentry', 'joke', 'vtip']
|
||||
for class_name in potential_classes:
|
||||
elements = soup.find_all(class_=lambda x: x and class_name in x.split())
|
||||
if elements:
|
||||
for el in elements:
|
||||
for br in el.find_all("br"):
|
||||
br.replace_with("\n")
|
||||
text = el.get_text()
|
||||
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
||||
text = "\n".join(lines)
|
||||
# Filter out very short texts (titles, metadata) and ensure safety limit
|
||||
if len(text) > 20 and len(lines) <= 20:
|
||||
jokes.append(text)
|
||||
if jokes:
|
||||
break
|
||||
|
||||
# Strategy 2: Fallback to all paragraphs if no specific container found
|
||||
if not jokes:
|
||||
for p in soup.find_all('p'):
|
||||
for br in p.find_all("br"):
|
||||
br.replace_with("\n")
|
||||
text = p.get_text()
|
||||
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
||||
text = "\n".join(lines)
|
||||
if len(text) > 50 and len(lines) <= 20: # Assume jokes are somewhat long paragraphs
|
||||
jokes.append(text)
|
||||
|
||||
if jokes:
|
||||
return random.choice(jokes)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
class BestPageJokeSource(JokeSource):
|
||||
def get_name(self):
|
||||
return "bestpage.cz"
|
||||
|
||||
def fetch_joke(self):
|
||||
url = "https://bestpage.cz/vtipy/"
|
||||
try:
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (compatible; PrintServer/1.0)'}
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
# Older sites often use windows-1250 or iso-8859-2
|
||||
response.encoding = response.apparent_encoding
|
||||
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
jokes = []
|
||||
|
||||
# Bestpage is an older site, often using tables or simple paragraphs
|
||||
for el in soup.find_all(['p', 'div', 'td']):
|
||||
for br in el.find_all("br"):
|
||||
br.replace_with("\n")
|
||||
text = el.get_text()
|
||||
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
||||
text = "\n".join(lines)
|
||||
|
||||
if 50 < len(text) < 1000 and len(lines) <= 20:
|
||||
jokes.append(text)
|
||||
|
||||
if jokes:
|
||||
return random.choice(jokes)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
31
jobs/maze.py
31
jobs/maze.py
@ -3,15 +3,33 @@ from PIL import Image, ImageDraw
|
||||
from .base import Job
|
||||
|
||||
class MazeJob(Job):
|
||||
def __init__(self):
|
||||
self.width = 14
|
||||
self.height = 32
|
||||
|
||||
def get_name(self):
|
||||
return "BLUDISTE"
|
||||
|
||||
def configure(self):
|
||||
print("\nSelect Difficulty:")
|
||||
print(" [1] Easy")
|
||||
print(" [2] Medium")
|
||||
print(" [3] Hard")
|
||||
choice = input("Choice [2]: ").strip()
|
||||
|
||||
if choice == '1':
|
||||
self.height = 8
|
||||
elif choice == '3':
|
||||
self.height = 32
|
||||
else:
|
||||
self.height = 18
|
||||
|
||||
def print_body(self, p):
|
||||
# Width and Height in cells.
|
||||
# Total width in chars = 2 * w + 1.
|
||||
# w=15 -> 31 chars (Fits comfortably on 80mm printers, tight on 58mm)
|
||||
w = 14
|
||||
h = 32
|
||||
w = self.width
|
||||
h = self.height
|
||||
|
||||
maze = self.generate_maze(w, h)
|
||||
|
||||
@ -38,12 +56,17 @@ class MazeJob(Job):
|
||||
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
x = c * cell_size
|
||||
y = r * cell_size
|
||||
|
||||
# Draw walls as black rectangles
|
||||
if maze[r][c] == '#':
|
||||
x = c * cell_size
|
||||
y = r * cell_size
|
||||
# fill=0 means Black in '1' mode
|
||||
draw.rectangle([x, y, x + cell_size, y + cell_size], fill=0)
|
||||
elif maze[r][c] == 'S':
|
||||
draw.text((x + 5, y + 2), "S", fill=0)
|
||||
elif maze[r][c] == 'E':
|
||||
draw.text((x + 5, y + 2), "E", fill=0)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
239
jobs/maze_multitarget.py
Normal file
239
jobs/maze_multitarget.py
Normal file
@ -0,0 +1,239 @@
|
||||
import random
|
||||
from collections import deque
|
||||
import time
|
||||
|
||||
class MazeMultitargetJob:
|
||||
def __init__(self):
|
||||
self.options = []
|
||||
self.correct_index = 0
|
||||
self.width = 18 * 2 + 1
|
||||
self.height = 20 * 2 + 1
|
||||
|
||||
def get_name(self):
|
||||
return "Maze with Multiple Endings"
|
||||
|
||||
def configure(self):
|
||||
print("\n--- Configure Maze Options ---")
|
||||
self.options = []
|
||||
print("Enter labels for the endings (empty line to finish):")
|
||||
while True:
|
||||
label = input(f"Option {chr(65 + len(self.options))}: ").strip()
|
||||
if not label:
|
||||
if len(self.options) < 2:
|
||||
print("Please enter at least 2 options.")
|
||||
continue
|
||||
break
|
||||
self.options.append(label)
|
||||
if len(self.options) >= 26:
|
||||
break
|
||||
|
||||
print("\nWhich option is the correct one?")
|
||||
for i, opt in enumerate(self.options):
|
||||
print(f" [{chr(65 + i)}] {opt}")
|
||||
|
||||
while True:
|
||||
choice = input("Correct option (letter): ").strip().upper()
|
||||
if len(choice) == 1:
|
||||
idx = ord(choice) - 65
|
||||
if 0 <= idx < len(self.options):
|
||||
self.correct_index = idx
|
||||
break
|
||||
print("Invalid selection.")
|
||||
|
||||
def run(self, printer):
|
||||
# 1. Generate Perfect Maze (DFS)
|
||||
# Grid: 1 = Wall, 0 = Path
|
||||
grid = [[1 for _ in range(self.width)] for _ in range(self.height)]
|
||||
|
||||
def get_neighbors(r, c, dist=2):
|
||||
ns = []
|
||||
for dr, dc in [(-dist, 0), (dist, 0), (0, -dist), (0, dist)]:
|
||||
nr, nc = r + dr, c + dc
|
||||
if 0 < nr < self.height and 0 < nc < self.width:
|
||||
ns.append((nr, nc))
|
||||
return ns
|
||||
|
||||
# Start carving from (1, 1)
|
||||
start_pos = (1, 1)
|
||||
grid[start_pos[0]][start_pos[1]] = 0
|
||||
stack = [start_pos]
|
||||
|
||||
while stack:
|
||||
current = stack[-1]
|
||||
r, c = current
|
||||
neighbors = get_neighbors(r, c)
|
||||
unvisited = []
|
||||
for nr, nc in neighbors:
|
||||
if grid[nr][nc] == 1:
|
||||
unvisited.append((nr, nc))
|
||||
|
||||
if unvisited:
|
||||
nr, nc = random.choice(unvisited)
|
||||
# Remove wall between
|
||||
wr, wc = (r + nr) // 2, (c + nc) // 2
|
||||
grid[wr][wc] = 0
|
||||
grid[nr][nc] = 0
|
||||
stack.append((nr, nc))
|
||||
else:
|
||||
stack.pop()
|
||||
|
||||
def find_path(start, end, current_grid):
|
||||
q = deque([start])
|
||||
came_from = {start: None}
|
||||
while q:
|
||||
curr = q.popleft()
|
||||
if curr == end:
|
||||
break
|
||||
|
||||
r, c = curr
|
||||
# Check neighbors (dist 1)
|
||||
for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||
nr, nc = r + dr, c + dc
|
||||
if 0 <= nr < self.height and 0 <= nc < self.width:
|
||||
if current_grid[nr][nc] == 0 and (nr, nc) not in came_from:
|
||||
came_from[(nr, nc)] = curr
|
||||
q.append((nr, nc))
|
||||
|
||||
if end not in came_from:
|
||||
return None
|
||||
|
||||
# Reconstruct path
|
||||
path = []
|
||||
curr = end
|
||||
while curr:
|
||||
path.append(curr)
|
||||
curr = came_from[curr]
|
||||
return path[::-1]
|
||||
|
||||
# 2. Place Endpoints
|
||||
# We need len(self.options) endpoints.
|
||||
endpoints = [None] * len(self.options)
|
||||
|
||||
# First, place the correct endpoint
|
||||
attempts = 0
|
||||
while endpoints[self.correct_index] is None and attempts < 1000:
|
||||
r = random.randrange(1, self.height, 2)
|
||||
c = random.randrange(1, self.width, 2)
|
||||
if (r, c) != start_pos and grid[r][c] == 0:
|
||||
endpoints[self.correct_index] = (r, c)
|
||||
attempts += 1
|
||||
|
||||
correct_endpoint = endpoints[self.correct_index]
|
||||
if not correct_endpoint:
|
||||
printer.text("Error: Could not place correct endpoint.\n")
|
||||
return
|
||||
|
||||
# Calculate true path to ensure we don't place fakes on it
|
||||
true_path = find_path(start_pos, correct_endpoint, grid)
|
||||
if not true_path:
|
||||
printer.text("Error: No path to correct endpoint.\n")
|
||||
return
|
||||
true_path_set = set(true_path)
|
||||
|
||||
# Place fake endpoints
|
||||
attempts = 0
|
||||
while None in endpoints and attempts < 2000:
|
||||
r = random.randrange(1, self.height, 2)
|
||||
c = random.randrange(1, self.width, 2)
|
||||
pt = (r, c)
|
||||
if pt != start_pos and pt not in endpoints and grid[r][c] == 0:
|
||||
if pt not in true_path_set:
|
||||
# Fill first empty slot
|
||||
for i in range(len(endpoints)):
|
||||
if endpoints[i] is None:
|
||||
endpoints[i] = pt
|
||||
break
|
||||
attempts += 1
|
||||
|
||||
if None in endpoints:
|
||||
printer.text("Error: Could not place enough endpoints.\n")
|
||||
return
|
||||
|
||||
# 4. Block Incorrect Paths
|
||||
# Robust Multi-target isolation
|
||||
|
||||
fakes = [pt for i, pt in enumerate(endpoints) if i != self.correct_index]
|
||||
|
||||
def get_degree(r, c, current_grid):
|
||||
deg = 0
|
||||
for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||
nr, nc = r + dr, c + dc
|
||||
if 0 <= nr < self.height and 0 <= nc < self.width:
|
||||
if current_grid[nr][nc] == 0:
|
||||
deg += 1
|
||||
return deg
|
||||
|
||||
for fake in fakes:
|
||||
# Repeat until fake is isolated from all other targets
|
||||
while True:
|
||||
connected_target = None
|
||||
path_to_target = None
|
||||
|
||||
# Prioritize connection to Start/Correct (Main), then other fakes
|
||||
check_order = [start_pos, correct_endpoint] + [f for f in fakes if f != fake]
|
||||
|
||||
for other in check_order:
|
||||
path = find_path(other, fake, grid)
|
||||
if path:
|
||||
connected_target = other
|
||||
path_to_target = path
|
||||
break
|
||||
|
||||
if not connected_target:
|
||||
break # Isolated from everyone
|
||||
|
||||
# Identify segment NOT on true_path
|
||||
valid_segment_start = 0
|
||||
for k in range(len(path_to_target)):
|
||||
if path_to_target[k] not in true_path_set:
|
||||
valid_segment_start = k
|
||||
break
|
||||
|
||||
if valid_segment_start == len(path_to_target):
|
||||
break # Should not happen unless fake is ON true path
|
||||
|
||||
# Find last junction on the path to maximize false path length
|
||||
best_cut_u_index = -1
|
||||
start_search = max(0, valid_segment_start - 1)
|
||||
|
||||
for k in range(start_search, len(path_to_target) - 1):
|
||||
u = path_to_target[k]
|
||||
if get_degree(u[0], u[1], grid) > 2:
|
||||
best_cut_u_index = k
|
||||
|
||||
if best_cut_u_index != -1:
|
||||
block_index = best_cut_u_index + 1
|
||||
else:
|
||||
block_index = len(path_to_target) - 2
|
||||
|
||||
# Ensure we block a valid node
|
||||
block_index = max(block_index, valid_segment_start)
|
||||
to_block = path_to_target[block_index]
|
||||
grid[to_block[0]][to_block[1]] = 1
|
||||
|
||||
# 5. Print Maze
|
||||
printer.text("Najdi spravny cil!\n\n")
|
||||
|
||||
# Map endpoints to letters
|
||||
endpoint_map = {pt: chr(65 + i) for i, pt in enumerate(endpoints)}
|
||||
|
||||
for r in range(self.height):
|
||||
line = ""
|
||||
for c in range(self.width):
|
||||
if (r, c) == start_pos:
|
||||
line += "S"
|
||||
elif (r, c) in endpoint_map:
|
||||
line += endpoint_map[(r, c)]
|
||||
elif grid[r][c] == 1:
|
||||
line += "█" # Full block
|
||||
else:
|
||||
line += " "
|
||||
printer.text(line + "\n")
|
||||
time.sleep(0.01)
|
||||
|
||||
printer.text("\nMoznosti:\n")
|
||||
for i, opt in enumerate(self.options):
|
||||
printer.text(f"{chr(65 + i)}: {opt}\n")
|
||||
|
||||
printer.text("\n\n")
|
||||
printer.cut()
|
||||
147
jobs/word_search.py
Normal file
147
jobs/word_search.py
Normal file
@ -0,0 +1,147 @@
|
||||
import random
|
||||
import time
|
||||
import textwrap
|
||||
from jobs.czech_words import CZECH_WORDS
|
||||
|
||||
class WordSearchJob:
|
||||
def __init__(self):
|
||||
self.width = 14
|
||||
self.height = 14
|
||||
self.hidden_phrase = "TAJENKA"
|
||||
self.words_to_find = []
|
||||
self.grid = []
|
||||
|
||||
def get_name(self):
|
||||
return "Word Search (Osmismerka)"
|
||||
|
||||
def configure(self):
|
||||
print("\n--- Configure Word Search ---")
|
||||
phrase = input("Enter hidden phrase (default: Random): ").strip().upper()
|
||||
if phrase:
|
||||
self.hidden_phrase = "".join(c for c in phrase if c.isalnum())
|
||||
else:
|
||||
self.hidden_phrase = random.choice(CZECH_WORDS)
|
||||
self.width = len(self.hidden_phrase) + 2
|
||||
self.height = len(self.hidden_phrase) + 2
|
||||
size_str = input(f"Enter size (default: {self.width}): ").strip()
|
||||
if size_str:
|
||||
try:
|
||||
size = int(size_str)
|
||||
self.width = size
|
||||
self.height = size
|
||||
except ValueError:
|
||||
pass
|
||||
if self.width < 5:
|
||||
self.width = 5
|
||||
self.height = 5
|
||||
if self.width > 40:
|
||||
self.width = 40
|
||||
self.height = 40
|
||||
|
||||
def run(self, printer):
|
||||
# Directions: Right, Down, Diag-Down-Right
|
||||
directions = [(0, 1), (1, 0), (1, 1)]
|
||||
target_empty = len(self.hidden_phrase)
|
||||
|
||||
# Retry loop to ensure perfect fill (no leftover letters)
|
||||
for attempt in range(20):
|
||||
# Initialize grid
|
||||
self.grid = [['' for _ in range(self.width)] for _ in range(self.height)]
|
||||
used_mask = [[False for _ in range(self.width)] for _ in range(self.height)]
|
||||
|
||||
# Filter words that fit in the grid
|
||||
available_words = [w for w in CZECH_WORDS if len(w) <= self.width and len(w) <= self.height]
|
||||
random.shuffle(available_words)
|
||||
|
||||
self.words_to_find = []
|
||||
|
||||
total_cells = self.width * self.height
|
||||
current_empty = total_cells
|
||||
|
||||
# Try to place words until we reach target_empty
|
||||
for word in available_words:
|
||||
if current_empty == target_empty:
|
||||
break
|
||||
|
||||
placed = False
|
||||
attempts = 0
|
||||
while not placed and attempts < 50:
|
||||
attempts += 1
|
||||
direction = random.choice(directions)
|
||||
dr, dc = direction
|
||||
|
||||
# Determine bounds
|
||||
if dr == 0: # Horizontal
|
||||
r = random.randint(0, self.height - 1)
|
||||
c = random.randint(0, self.width - len(word))
|
||||
elif dc == 0: # Vertical
|
||||
r = random.randint(0, self.height - len(word))
|
||||
c = random.randint(0, self.width - 1)
|
||||
else: # Diagonal
|
||||
r = random.randint(0, self.height - len(word))
|
||||
c = random.randint(0, self.width - len(word))
|
||||
|
||||
# Check collision
|
||||
fits = True
|
||||
temp_new_cells = 0
|
||||
for i, char in enumerate(word):
|
||||
nr, nc = r + i*dr, c + i*dc
|
||||
# Cell must be empty OR contain the same letter
|
||||
if self.grid[nr][nc] != '' and self.grid[nr][nc] != char:
|
||||
fits = False
|
||||
break
|
||||
if self.grid[nr][nc] == '':
|
||||
temp_new_cells += 1
|
||||
|
||||
if fits:
|
||||
# Check if this overfills
|
||||
if current_empty - temp_new_cells < target_empty:
|
||||
continue
|
||||
|
||||
# Place it
|
||||
for i, char in enumerate(word):
|
||||
nr, nc = r + i*dr, c + i*dc
|
||||
if self.grid[nr][nc] == '':
|
||||
current_empty -= 1
|
||||
self.grid[nr][nc] = char
|
||||
used_mask[nr][nc] = True
|
||||
self.words_to_find.append(word)
|
||||
placed = True
|
||||
|
||||
if current_empty == target_empty:
|
||||
break
|
||||
|
||||
# Fill remaining spots with hidden phrase
|
||||
empty_slots = []
|
||||
for r in range(self.height):
|
||||
for c in range(self.width):
|
||||
if not used_mask[r][c]:
|
||||
empty_slots.append((r, c))
|
||||
|
||||
phrase_idx = 0
|
||||
for r, c in empty_slots:
|
||||
if phrase_idx < len(self.hidden_phrase):
|
||||
self.grid[r][c] = self.hidden_phrase[phrase_idx]
|
||||
phrase_idx += 1
|
||||
else:
|
||||
# Fill with random letters if phrase is done (extra filler)
|
||||
self.grid[r][c] = random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
# Print Header
|
||||
printer.text("OSMISMĚRKA\n")
|
||||
printer.text("Najdi slova a přečti tajenku!\n")
|
||||
if current_empty != target_empty:
|
||||
printer.text(f"Tajenka má {len(self.hidden_phrase)} písmen.\n")
|
||||
printer.text("\n")
|
||||
|
||||
# Print Grid
|
||||
for r in range(self.height):
|
||||
line = " ".join(self.grid[r])
|
||||
printer.text(line + "\n")
|
||||
time.sleep(0.05)
|
||||
|
||||
printer.text("\nSlova k hledání:\n")
|
||||
words_str = ", ".join(sorted(self.words_to_find))
|
||||
printer.text(textwrap.fill(words_str, width=42) + "\n")
|
||||
printer.text("\n\n")
|
||||
printer.cut()
|
||||
@ -1,9 +1,16 @@
|
||||
import time
|
||||
from escpos.printer import Usb, Dummy
|
||||
from escpos.exceptions import USBNotFoundError
|
||||
from jobs.math_homework import MathHomeworkJob
|
||||
from jobs.unit_conversion import UnitConversionJob
|
||||
from jobs.chess_puzzle import ChessPuzzleJob
|
||||
from jobs.maze import MazeJob
|
||||
from jobs.division_cipher import DivisionCipherJob
|
||||
from jobs.decimal_division import DecimalDivisionJob
|
||||
from jobs.joke import JokeJob
|
||||
from jobs.maze_multitarget import MazeMultitargetJob
|
||||
from jobs.flush import FlushJob
|
||||
from jobs.word_search import WordSearchJob
|
||||
|
||||
# ==========================================
|
||||
# CONFIGURATION
|
||||
@ -12,7 +19,13 @@ from jobs.maze import MazeJob
|
||||
# Example: 0x04b8 is Epson.
|
||||
USB_VENDOR_ID = 0x0525
|
||||
USB_PRODUCT_ID = 0xa700
|
||||
# Input interface:
|
||||
# `lsusb -vvv -d xxxx:xxxx | grep iInterface`
|
||||
# iInterface 0
|
||||
INPUT_ENDPOINT = 0x00
|
||||
# Output interface:
|
||||
# `lsusb -vvv -d xxxx:xxxx | grep bEndpointAddress | grep OUT`
|
||||
# bEndpointAddress 0x01 EP 1 OUT
|
||||
OUTPUT_ENDPOINT = 0x01
|
||||
|
||||
# Set to True to print to console instead of physical printer (for testing)
|
||||
@ -28,12 +41,12 @@ def get_printer():
|
||||
|
||||
try:
|
||||
# Initialize USB printer
|
||||
# profile="TM-T88V" is a generic profile, works for many ESC/POS printers
|
||||
|
||||
# Generic printer with custom settings:
|
||||
p = Usb(USB_VENDOR_ID, USB_PRODUCT_ID, 0, INPUT_ENDPOINT, OUTPUT_ENDPOINT, profile="default")
|
||||
p.profile.profile_data['media']['width']['mm'] = 80
|
||||
p.profile.profile_data['media']['width']['pixels'] = 512
|
||||
|
||||
# Specific printer based on a profile. See https://python-escpos.readthedocs.io/en/latest/printer_profiles/available-profiles.html
|
||||
# p = Usb(USB_VENDOR_ID, USB_PRODUCT_ID, 0, INPUT_ENDPOINT, OUTPUT_ENDPOINT, profile="TM-T88V")
|
||||
return p
|
||||
except USBNotFoundError:
|
||||
@ -47,7 +60,14 @@ JOBS = [
|
||||
MathHomeworkJob(),
|
||||
UnitConversionJob(),
|
||||
ChessPuzzleJob(),
|
||||
MazeJob()
|
||||
MazeJob(),
|
||||
DivisionCipherJob(),
|
||||
DecimalDivisionJob(),
|
||||
JokeJob(),
|
||||
MazeMultitargetJob(),
|
||||
WordSearchJob(),
|
||||
# keep this last:
|
||||
FlushJob()
|
||||
]
|
||||
|
||||
def run_tui():
|
||||
@ -79,21 +99,33 @@ def run_tui():
|
||||
continue
|
||||
|
||||
if job:
|
||||
job.configure()
|
||||
|
||||
copies_str = input("\nNumber of copies [1]: ").strip()
|
||||
try:
|
||||
copies = max(1, int(copies_str)) if copies_str else 1
|
||||
except ValueError:
|
||||
copies = 1
|
||||
|
||||
p = get_printer()
|
||||
if p:
|
||||
print(f"Printing {job.get_name()}...")
|
||||
print(f"Printing {job.get_name()} ({copies} copies)...")
|
||||
try:
|
||||
job.run(p)
|
||||
for i in range(copies):
|
||||
if copies > 1:
|
||||
print(f" Printing copy {i + 1}...")
|
||||
job.run(p)
|
||||
|
||||
# If using Dummy, print the output to console to verify
|
||||
if isinstance(p, Dummy):
|
||||
print(p.output.decode('utf-8', errors='ignore'))
|
||||
# If using Dummy, print the output to console to verify
|
||||
if isinstance(p, Dummy):
|
||||
print(p.output.decode('utf-8', errors='ignore'))
|
||||
|
||||
print("Success! Job sent to printer.")
|
||||
except Exception as e:
|
||||
print(f"Print Error: {e}")
|
||||
finally:
|
||||
if not isinstance(p, Dummy):
|
||||
time.sleep(0.5)
|
||||
p.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Loading…
Reference in New Issue
Block a user