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()
 |