285 lines
8.3 KiB
Python
285 lines
8.3 KiB
Python
|
#!/usr/bin/env python
|
||
|
# -*- coding: utf-8 -*-
|
||
|
"""
|
||
|
This module can be used to create and display Lycacodes
|
||
|
|
||
|
(C) 2022 Louis Heredero louis.heredero@edu.vs.ch
|
||
|
"""
|
||
|
|
||
|
import pygame
|
||
|
import numpy as np
|
||
|
import hamming
|
||
|
|
||
|
S = 600
|
||
|
|
||
|
class LycacodeError(Exception):
|
||
|
pass
|
||
|
|
||
|
class Lycacode:
|
||
|
RES = 3
|
||
|
BLOCKSIZE = 7
|
||
|
|
||
|
MODE_PERSON = 0
|
||
|
MODE_LOC = 1
|
||
|
MODE_LINK = 2
|
||
|
MODE_TEXT = 3
|
||
|
|
||
|
PERSON_STUDENT = 0
|
||
|
PERSON_TEACHER = 1
|
||
|
PERSON_OTHER = 2
|
||
|
|
||
|
BLACK = (158,17,26)
|
||
|
#BLACK = (0,0,0)
|
||
|
WHITE = (255,255,255)
|
||
|
|
||
|
OFFSETS = [(0,-1), (1,0), (0,1), (-1,0)]
|
||
|
|
||
|
MASKS = [
|
||
|
lambda x, y: x%3 == 0,
|
||
|
lambda x, y: y%3 == 0,
|
||
|
lambda x, y: (x+y)%3 == 0,
|
||
|
lambda x, y: (x%3)*(y%3)==0,
|
||
|
lambda x, y: (y//3+x//3)%2==0,
|
||
|
lambda x, y: (y%3-1)*(x%3-y%3-2)*(y%3-x%3-2)==0,
|
||
|
lambda x, y: (abs(13-x)+abs(13-y))%3==1,
|
||
|
lambda x, y: (1-x%2 + max(0, abs(13-y)-abs(13-x))) * (1-y%2 + max(0,abs(13-x)-abs(13-y))) == 0
|
||
|
]
|
||
|
|
||
|
FRAME = True
|
||
|
CIRCLES = True
|
||
|
DOTS = True
|
||
|
DB_SQUARES = False
|
||
|
|
||
|
def __init__(self, data, mode):
|
||
|
self.data = data
|
||
|
self.mode = mode
|
||
|
self.encode()
|
||
|
self.create_matrix()
|
||
|
|
||
|
def encode(self):
|
||
|
self.bits = f"{self.mode:02b}"
|
||
|
|
||
|
if self.mode == self.MODE_PERSON:
|
||
|
type_ = self.data["type"]
|
||
|
id_ = self.data["id"]
|
||
|
self.bits += f"{type_:02b}"
|
||
|
self.bits += f"{id_:020b}"
|
||
|
|
||
|
if type_ == self.PERSON_STUDENT:
|
||
|
year = self.data["year"]
|
||
|
class_ = self.data["class"]
|
||
|
self.bits += f"{year:03b}"
|
||
|
self.bits += f"{class_:04b}"
|
||
|
in1, in2 = self.data["initials"]
|
||
|
in1, in2 = ord(in1)-ord("A"), ord(in2)-ord("A")
|
||
|
self.bits += f"{in1:05b}"
|
||
|
self.bits += f"{in2:05b}"
|
||
|
# 83 left
|
||
|
|
||
|
elif type_ == self.PERSON_TEACHER:
|
||
|
# 100 left
|
||
|
pass
|
||
|
|
||
|
elif type_ == self.PERSON_OTHER:
|
||
|
# 100 left
|
||
|
pass
|
||
|
|
||
|
|
||
|
elif self.mode == self.MODE_LOC:
|
||
|
section = self.data["section"]
|
||
|
room = self.data["room"]
|
||
|
self.bits += f"{section:03b}"
|
||
|
self.bits += f"{room:09b}"
|
||
|
# 106 left
|
||
|
|
||
|
elif self.mode == self.MODE_LINK:
|
||
|
self.bits += f"{self.data:032b}"
|
||
|
# 86 left
|
||
|
|
||
|
elif self.mode == self.MODE_TEXT: # max 14 chars
|
||
|
data = self.data.encode("utf-8")
|
||
|
self.bits += f"{len(data):04b}"
|
||
|
self.bits += "".join(list(map(lambda b: f"{b:08b}", data)))
|
||
|
# 2 left
|
||
|
|
||
|
else:
|
||
|
raise LycacodeError(f"Invalid mode {self.mode}")
|
||
|
|
||
|
ds = self.BLOCKSIZE-self.BLOCKSIZE.bit_length()
|
||
|
total_bits = (self.RES**2 * 24 - 6)
|
||
|
data_bits = total_bits * ds // self.BLOCKSIZE
|
||
|
self.bits += "0"*(ds-len(self.bits)%ds)
|
||
|
s = ""
|
||
|
i = 0
|
||
|
left = data_bits-len(self.bits)
|
||
|
while len(s) < left:
|
||
|
s += f"{i:0b}"
|
||
|
i += 1
|
||
|
s = s[:left]
|
||
|
self.bits += s
|
||
|
self.bits = hamming.encode(self.bits, self.BLOCKSIZE)
|
||
|
|
||
|
def create_matrix(self):
|
||
|
R = self.RES
|
||
|
self.matrix = np.zeros([R*9, R*9])-1
|
||
|
self.matrix[R*4:R*5, :] = 0
|
||
|
self.matrix[:, R*4:R*5] = 0
|
||
|
self.matrix[R:R*2, R*3:R*6] = 0
|
||
|
self.matrix[R*3:R*6, R:R*2] = 0
|
||
|
self.matrix[-R*2:-R, -R*6:-R*3] = 0
|
||
|
self.matrix[-R*6:-R*3, -R*2:-R] = 0
|
||
|
|
||
|
self.matrix[R*4:R*5,R*4:R*5] = -2
|
||
|
self.matrix[0, R*4:R*5] = -2 # mask
|
||
|
self.matrix[-1, R*4:R*5] = -2 # mask
|
||
|
mask_area = np.where(self.matrix == 0, 1, 0)
|
||
|
self.matrix[R*4, R*4+1:R*5-1] = 1
|
||
|
self.matrix[R*5-1, R*4] = 1
|
||
|
self.matrix[R*4+1:R*5-1, R*4+1:R*5-1] = 1
|
||
|
|
||
|
bits = list(map(int, self.bits))
|
||
|
bits = np.reshape(bits, [-1,self.BLOCKSIZE]).T
|
||
|
bits = np.reshape(bits, [-1]).tolist()
|
||
|
|
||
|
for y in range(R*9):
|
||
|
for x in range(R*9):
|
||
|
if self.matrix[y,x] == 0:
|
||
|
self.matrix[y,x] = bits.pop(0)
|
||
|
|
||
|
if len(bits) == 0:
|
||
|
break
|
||
|
|
||
|
if len(bits) == 0:
|
||
|
break
|
||
|
|
||
|
self.matrix = np.where(self.matrix==-2, 0, self.matrix)
|
||
|
|
||
|
best = [None, None, None]
|
||
|
for i, mask in enumerate(self.MASKS):
|
||
|
score, matrix = self.evaluate(mask, mask_area)
|
||
|
if best[0] is None or score < best[0]:
|
||
|
best = (score, matrix, i)
|
||
|
|
||
|
self.matrix = best[1]
|
||
|
id_ = list(map(int, f"{best[2]:03b}"))
|
||
|
self.matrix[0, R*4:R*5] = id_ # mask
|
||
|
self.matrix[-1, R*4:R*5] = id_ # mask
|
||
|
|
||
|
def evaluate(self, mask, mask_area):
|
||
|
matrix = self.matrix.copy()
|
||
|
for y in range(self.matrix.shape[0]):
|
||
|
for x in range(self.matrix.shape[1]):
|
||
|
if mask_area[y][x] and mask(x,y):
|
||
|
matrix[y][x] = 1-matrix[y][x]
|
||
|
|
||
|
score = 0
|
||
|
|
||
|
# 3 or more of the same color (horizontal)
|
||
|
for y in range(self.matrix.shape[0]):
|
||
|
c = 0
|
||
|
col = -1
|
||
|
for x in range(self.matrix.shape[1]):
|
||
|
if matrix[y][x] == -1: continue
|
||
|
if col != matrix[y][x]:
|
||
|
c = 0
|
||
|
col = matrix[y][x]
|
||
|
c += 1
|
||
|
if c == 3:
|
||
|
score += 4
|
||
|
elif c > 3:
|
||
|
score += 2
|
||
|
|
||
|
# 3 or more of the same color (vertical)
|
||
|
for x in range(self.matrix.shape[1]):
|
||
|
c = 0
|
||
|
col = -1
|
||
|
for y in range(self.matrix.shape[0]):
|
||
|
if matrix[y][x] == -1: continue
|
||
|
if col != matrix[y][x]:
|
||
|
c = 0
|
||
|
col = matrix[y][x]
|
||
|
c += 1
|
||
|
if c == 3:
|
||
|
score += 4
|
||
|
elif c > 3:
|
||
|
score += 2
|
||
|
|
||
|
# 2x2 blocks of the same color
|
||
|
for y in range(matrix.shape[0]-1):
|
||
|
for x in range(matrix.shape[1]-1):
|
||
|
if matrix[y][x] == -1: continue
|
||
|
zone = matrix[y:y+2, x:x+2]
|
||
|
if np.all(zone == zone[0,0]):
|
||
|
score += 2
|
||
|
|
||
|
# more dots/1s => higher score
|
||
|
total = matrix.shape[0]*matrix.shape[1]
|
||
|
dots = np.sum(matrix == 1)
|
||
|
percent = 100*dots//total
|
||
|
score += percent//5 * 2
|
||
|
|
||
|
return score, matrix
|
||
|
|
||
|
def display(self, surf):
|
||
|
R = self.RES
|
||
|
S = min(surf.get_size())
|
||
|
s = int(S/12/R)*R
|
||
|
O = (S-s*9)/2
|
||
|
|
||
|
surf.fill(self.WHITE)
|
||
|
|
||
|
# Frame
|
||
|
if self.FRAME:
|
||
|
pygame.draw.rect(surf, self.BLACK, [O-s, O-s, s*11, s*11])
|
||
|
pygame.draw.rect(surf, self.WHITE, [O-s*0.5, O-s*0.5, s*10, s*10])
|
||
|
|
||
|
# Cross
|
||
|
for i in range(4):
|
||
|
dx, dy = self.OFFSETS[i]
|
||
|
X, Y = S/2 + dx*s*3, S/2 + dy*s*3
|
||
|
if self.CIRCLES:
|
||
|
for j in range(3):
|
||
|
dx2, dy2 = self.OFFSETS[(i+j-1)%4]
|
||
|
pygame.draw.circle(surf, self.BLACK, [X+dx2*s, Y+dy2*s], 0.75*s)
|
||
|
|
||
|
pygame.draw.rect(surf, self.BLACK, [X-(1.5-abs(dx))*s, Y-(1.5-abs(dy))*s, s*(3-abs(dx)*2), s*(3-abs(dy)*2)])
|
||
|
|
||
|
pygame.draw.rect(surf, self.BLACK, [O, S/2-s/2, 9*s, s])
|
||
|
pygame.draw.rect(surf, self.BLACK, [S/2-s/2, O, s, 9*s])
|
||
|
|
||
|
# Dots
|
||
|
if self.DOTS:
|
||
|
for y in range(R*9):
|
||
|
for x in range(R*9):
|
||
|
if self.matrix[y, x] == 1:
|
||
|
pygame.draw.circle(surf, self.WHITE, [O+(x+0.5)*s/R, O+(y+0.5)*s/R], s/R/3)
|
||
|
|
||
|
if self.DB_SQUARES:
|
||
|
for y in range(9):
|
||
|
for x in range(9):
|
||
|
if self.matrix[y*R, x*R] != -1:
|
||
|
pygame.draw.rect(surf, (0,0,0), [O+x*s, O+y*s, s+1, s+1], 2)
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
import base
|
||
|
|
||
|
b = base.Base(S, S, "Lycacode generator")
|
||
|
|
||
|
code = Lycacode({
|
||
|
"type": Lycacode.PERSON_STUDENT,
|
||
|
"id": 16048,
|
||
|
"year": 5,
|
||
|
"class": 3,
|
||
|
"initials": "LH"
|
||
|
}, Lycacode.MODE_PERSON)
|
||
|
|
||
|
#code = Lycacode("Embarquement12", Lycacode.MODE_TEXT)
|
||
|
"""code = Lycacode({
|
||
|
"section": 4,
|
||
|
"room": 209
|
||
|
}, Lycacode.MODE_LOC)"""
|
||
|
#code = Lycacode(1, Lycacode.MODE_LINK)
|
||
|
code.display(b.w)
|
||
|
|
||
|
b.main()
|