0
\$\begingroup\$

This question contains code of the updated version of my GUI Tic Tac Toe game, the code is about the styling and playing piece customization. It is related to: GUI Tic Tac Toe game with UNBEATABLE AI players and GUI Tic-Tac-Toe game with six AI players - part 2: the styling

enter image description here

Much of the code is reused, I wrote the code for the entire project all by myself, according to an array of resources I found from all over the internet, I found equations for the color space transformations and blend modes, and simplified the equations and implemented the equations as efficiently as I can.

I wrote the code that generates XYZ-RGB color space conversion transformation matrix, to get transformation matrices with unrounded machine native double precision values.

color.py contains code of two way conversions between RGB-HSL, RGB-HSV, RGB-CIELChab(D65), and RGB-CIELChab(D50) color spaces, and as I mentioned earlier I viewed a huge number of webpages to find the equations and implement the code all by myself. The color space conversions are later used by several of the functions that implement many standard blend modes (the non-separable blend modes that consider each pixel as a whole color rather than processing individual color channels).

blend_modes.py contains code of many common blend modes, there are 27 of them, I implemented them all by myself according to various sources online, as mentioned earlier. They are quite efficient because I use numba, most can process one 1920x1080 image in about 20 milliseconds, but color space conversions involving CIE-LCh take about 200 milliseconds to process one 1080P image.

But because the pieces I used are all 100x100 images which are quite small, the conversion is much faster, separable blend modes take about 60 microseconds, non-separable blend modes involving HSL/HSV conversions take about 250 microseconds, and LCh blend modes take about 3 milliseconds. And because I use numba, all relevant functions need to be compiled first called, the compilation takes some time, but this ensures every subsequent calls of the same functions are fast, this will cause the GUI to become unresponsive when you change to a blend mode that isn't selected before.

If you mouse scroll through all the blend modes, this will cause all the blend modes to compile and a significant stutter in the GUI, but this will also eliminate all future lags when you want to change blend modes, so that the change of blend mode is applied seeming instantly.

Lastly I removed the GUI customization, namely the window below, because the code becomes way too complicated in order to facilitate this functionality in the way I originally envisioned it, and there was way too long code for this functionality alone, and the code remains a big contributor of the bugs I have found. Also because way too many widgets are customizable way too many variables need to be kept track of and create a widget for its customization, the interface becomes way too complicated and ugly, it is a chore to navigate through the GUI and the window has a resolution of 1304x1004, my screen is 1920x1080 though I guess many of you might have smaller screens.

Instead I just stored the style in a very long string that is a CSS, the CSS is stored in a text file and it contains styles for all the windows, and there are slots? (I don't know the name, it is {folder}) for the absolute path of the root directory of the game, that is initially a placeholder. When the game is run, the placeholder is substituted by the actual absolute path of the script file's parent folder, this is to make the program always find the icons regardless of where the root directory is. Of course, if you want to customize the GUI you can change the CSS yourself, if you are techie enough.

compute_XYZ_RGB_matrices.py

(not part of the project itself, but closely related)

import numpy as np
from typing import Tuple


def xy_to_XY(x: float, y: float) -> Tuple[float]:
    return x / y, (1 - x - y) / y


BRADFORD = np.array(
    [
        [0.8951000, 0.2664000, -0.1614000],
        [-0.7502000, 1.7135000, 0.0367000],
        [0.0389000, -0.0685000, 1.0296000],
    ],
    dtype=float,
)

BRADFORD_INV = np.linalg.inv(BRADFORD)

D65 = np.array([0.9504559270516716, 1, 1.0890577507598784], dtype=float)
D50 = np.array([0.96420288, 1, 0.82490540], dtype=float)
CHAD = np.zeros(9)
CHAD[:9:4] = (BRADFORD @ D50) / (BRADFORD @ D65)
CHAD = BRADFORD_INV @ CHAD.reshape((3, 3)) @ BRADFORD


Rx = 0.639998686
Ry = 0.330010138
Gx = 0.300003784
Gy = 0.600003357
Bx = 0.150002046
By = 0.059997204

Xr, Zr = xy_to_XY(Rx, Ry)
Xg, Zg = xy_to_XY(Gx, Gy)
Xb, Zb = xy_to_XY(Bx, By)
INTERIMAT = np.array([[Xr, Xg, Xb], [1, 1, 1], [Zr, Zg, Zb]], dtype=float)
MATINV = np.linalg.inv(INTERIMAT)
D65_Y = MATINV @ D65
D65_XYZ = INTERIMAT * D65_Y


def xyY_to_XYZ(x: float, y: float, Y: float) -> Tuple[float]:
    return x * Y / y, Y, (1 - x - y) * Y / y


D50_XYZ = np.vstack(
    [
        CHAD @ xyY_to_XYZ(Rx, Ry, D65_Y[0]),
        CHAD @ xyY_to_XYZ(Gx, Gy, D65_Y[1]),
        CHAD @ xyY_to_XYZ(Bx, By, D65_Y[2]),
    ]
).T

D65_RGB = np.linalg.inv(D65_XYZ)
D50_RGB = np.linalg.inv(D50_XYZ)

for i, c in enumerate("XYZ"):
    for j, d in enumerate("rgb"):
        print(f"D65_{c}{d} = {D65_XYZ[i, j]}")

print()
for i, c in enumerate("RGB"):
    for j, d in enumerate("xyz"):
        print(f"D65_{c}{d} = {D65_RGB[i, j]}")

print()
for i, c in enumerate("XYZ"):
    for j, d in enumerate("rgb"):
        print(f"D50_{c}{d} = {D50_XYZ[i, j]}")

print()
for i, c in enumerate("RGB"):
    for j, d in enumerate("xyz"):
        print(f"D50_{c}{d} = {D50_RGB[i, j]}")

color.py

import numba as nb
import numpy as np
from math import atan2, cos, sin, pi
from typing import Callable, Tuple

D65_Xw = 0.9504559270516716
D65_Zw = 1.0890577507598784
D50_Xw = 0.96420288
D50_Zw = 0.82490540

D65_Xr = 0.4123835774573348
D65_Xg = 0.35758636076837935
D65_Xb = 0.18048598882595746
D65_Yr = 0.21264225112116675
D65_Yg = 0.7151677022795175
D65_Yb = 0.07219004659931565
D65_Zr = 0.019324834131038457
D65_Zg = 0.11918543851645445
D65_Zb = 0.9505474781123853

D65_Rx = 3.2410639132702483
D65_Ry = -1.5374434989773638
D65_Rz = -0.49863738352233855
D65_Gx = -0.9692888172936756
D65_Gy = 1.875993314670902
D65_Gz = 0.04157078604801982
D65_Bx = 0.05564381729909414
D65_By = -0.20396692403457678
D65_Bz = 1.0569503107394616

D50_Xr = 0.43603484825656935
D50_Xg = 0.3851166943865836
D50_Xb = 0.14305133735684697
D50_Yr = 0.22248792538138254
D50_Yg = 0.7169037981483454
D50_Yb = 0.06060827647027179
D50_Zr = 0.013915901710730823
D50_Zg = 0.09706054515938503
D50_Zb = 0.7139289531298839

D50_Rx = 3.1342757757453534
D50_Ry = -1.6172769674977776
D50_Rz = -0.4907238602027905
D50_Gx = -0.9787936355010586
D50_Gy = 1.9161606866577585
D50_Gz = 0.033452266912100515
D50_Bx = 0.07197630801411643
D50_By = -0.2289831961913926
D50_Bz = 1.4057168648822214

LAB_F0 = 216 / 24389
LAB_F1 = 841 / 108
LAB_F2 = 4 / 29
LAB_F3 = LAB_F0 ** (1 / 3)
LAB_F4 = 1 / LAB_F1
LAB_F5 = 1 / 2.4
LAB_F6 = 0.04045 / 12.92
RtD = 180 / pi
DtR = pi / 180


@nb.njit(cache=True, fastmath=True)
def extrema(a: float, b: float, c: float) -> Tuple[float]:
    i = 2
    if b > c:
        b, c = c, b
        i = 1

    if a > b:
        a, b = b, a

    if b > c:
        b, c = c, b
        i = 0

    return i, a, c


@nb.njit(cache=True, fastmath=True)
def hue(r: float, g: float, b: float, d: float, i: float) -> float:
    if i == 2:
        h = 2 / 3 + (r - g) / (6 * d)
    elif i:
        h = 1 / 3 + (b - r) / (6 * d)
    else:
        h = (g - b) / (6 * d)

    return h % 1


@nb.njit(cache=True, fastmath=True)
def HSL_pixel(r: float, g: float, b: float) -> Tuple[float]:
    i, x, z = extrema(r, g, b)
    s = x + z
    d = z - x
    avg = s / 2

    return (hue(r, g, b, d, i), d / (1 - abs(s - 1)), avg) if d else (0, 0, avg)


@nb.njit(cache=True, fastmath=True)
def HSV_pixel(r: float, g: float, b: float) -> Tuple[float]:
    i, x, z = extrema(r, g, b)
    d = z - x
    return (hue(r, g, b, d, i), d / z, z) if d else (0, 0, z)


@nb.njit(cache=True, parallel=True)
def convert(img: np.ndarray, mode: Callable) -> np.ndarray:
    height, width = img.shape[:2]
    out = np.empty_like(img)
    for y in nb.prange(height):
        for x in nb.prange(width):
            a, b, c = img[y, x]
            out[y, x] = mode(a, b, c)

    return out


@nb.njit
def RGB_to_HSL(img: np.ndarray) -> np.ndarray:
    return convert(img, HSL_pixel)


@nb.njit
def RGB_to_HSV(img: np.ndarray) -> np.ndarray:
    return convert(img, HSV_pixel)


@nb.njit(cache=True, fastmath=True)
def HSL_helper(h: float, n: float) -> float:
    k = (n + 12 * h) % 12
    return max(-1, min(k - 3, 9 - k, 1))


@nb.njit(cache=True, fastmath=True)
def RGB_from_HSL_pixel(h: float, s: float, l: float):
    a = s * min(l, 1 - l)
    return l - a * HSL_helper(h, 0), l - a * HSL_helper(h, 8), l - a * HSL_helper(h, 4)


@nb.njit(cache=True, fastmath=True)
def HSV_helper(h: float, n: float) -> float:
    k = (n + 6 * h) % 6
    return max(0, min(k, 4 - k, 1))


@nb.njit(cache=True, fastmath=True)
def RGB_from_HSV_pixel(h: float, s: float, v: float):
    a = v * s
    return v - a * HSV_helper(h, 5), v - a * HSV_helper(h, 3), v - a * HSV_helper(h, 1)


@nb.njit(cache=True)
def HSL_short(h: float, s: float, l: float) -> Tuple[float]:
    return RGB_from_HSL_pixel(h, s, l) if s else (l, l, l)


@nb.njit(cache=True)
def HSV_short(h: float, s: float, v: float) -> Tuple[float]:
    return RGB_from_HSV_pixel(h, s, v) if s else (v, v, v)


@nb.njit
def HSL_to_RGB(hsl: np.ndarray) -> np.ndarray:
    return convert(hsl, HSL_short)


@nb.njit
def HSV_to_RGB(hsv: np.ndarray) -> np.ndarray:
    return convert(hsv, HSV_short)


@nb.njit(cache=True, fastmath=True)
def gamma_expand(c: float) -> float:
    return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4


@nb.njit(cache=True, fastmath=True)
def LABF(f: float) -> float:
    return f ** (1 / 3) if f >= LAB_F0 else LAB_F1 * f + LAB_F2


@nb.njit(cache=True, fastmath=True)
def LABINVF(f: float) -> float:
    return f**3 if f >= LAB_F3 else LAB_F4 * (f - LAB_F2)


@nb.njit(cache=True, fastmath=True)
def gamma_contract(n: float) -> float:
    n = n * 12.92 if n <= LAB_F6 else (1.055 * n**LAB_F5) - 0.055
    return 0.0 if n < 0 else (1.0 if n > 1 else n)


@nb.njit(cache=True, fastmath=True)
def RGB_to_LCh_D65(r: float, g: float, b: float) -> Tuple[float]:
    b = gamma_expand(b)
    g = gamma_expand(g)
    r = gamma_expand(r)
    x = LABF((D65_Xr * r + D65_Xg * g + D65_Xb * b) / D65_Xw)
    y = LABF(D65_Yr * r + D65_Yg * g + D65_Yb * b)
    z = LABF((D65_Zr * r + D65_Zg * g + D65_Zb * b) / D65_Zw)
    m = 500 * (x - y)
    n = 200 * (y - z)
    return 116 * y - 16, (m * m + n * n) ** 0.5, (atan2(n, m) * RtD) % 360


@nb.njit(cache=True, fastmath=True)
def LCh_D65_to_RGB(l: float, c: float, h: float) -> Tuple[float]:
    h *= DtR
    l = (l + 16) / 116
    x = D65_Xw * LABINVF(l + c * cos(h) / 500)
    y = LABINVF(l)
    z = D65_Zw * LABINVF(l - c * sin(h) / 200)
    r = D65_Rx * x + D65_Ry * y + D65_Rz * z
    g = D65_Gx * x + D65_Gy * y + D65_Gz * z
    b = D65_Bx * x + D65_By * y + D65_Bz * z
    m = min(r, g, b)
    if m < 0:
        r -= m
        g -= m
        b -= m

    return gamma_contract(r), gamma_contract(g), gamma_contract(b)


@nb.njit(cache=True, fastmath=True)
def RGB_to_LCh_D50(r: float, g: float, b: float) -> Tuple[float]:
    b = gamma_expand(b)
    g = gamma_expand(g)
    r = gamma_expand(r)
    x = LABF((D50_Xr * r + D50_Xg * g + D50_Xb * b) / D50_Xw)
    y = LABF(D50_Yr * r + D50_Yg * g + D50_Yb * b)
    z = LABF((D50_Zr * r + D50_Zg * g + D50_Zb * b) / D50_Zw)
    m = 500 * (x - y)
    n = 200 * (y - z)
    return 116 * y - 16, (m * m + n * n) ** 0.5, (atan2(n, m) * RtD) % 360


@nb.njit(cache=True, fastmath=True)
def LCh_D50_to_RGB(l: float, c: float, h: float) -> Tuple[float]:
    h *= DtR
    l = (l + 16) / 116
    x = D50_Xw * LABINVF(l + c * cos(h) / 500)
    y = LABINVF(l)
    z = D50_Zw * LABINVF(l - c * sin(h) / 200)
    r = D50_Rx * x + D50_Ry * y + D50_Rz * z
    g = D50_Gx * x + D50_Gy * y + D50_Gz * z
    b = D50_Bx * x + D50_By * y + D50_Bz * z
    m = min(r, g, b)
    if m < 0:
        r -= m
        g -= m
        b -= m

    return gamma_contract(r), gamma_contract(g), gamma_contract(b)


@nb.njit(cache=True, parallel=True)
def loop_LCh(img: np.ndarray, mode: Callable) -> np.ndarray:
    height, width = img.shape[:2]
    out = np.empty_like(img)
    for y in nb.prange(height):
        for x in nb.prange(width):
            a, b, c = img[y, x]
            out[y, x] = mode(a, b, c)

    return out


@nb.njit
def IMG_to_LCh_D65(img: np.ndarray) -> np.ndarray:
    return loop_LCh(img, RGB_to_LCh_D65)


@nb.njit
def LCh_D65_to_IMG(lch: np.ndarray) -> np.ndarray:
    return loop_LCh(lch, LCh_D65_to_RGB)


@nb.njit
def IMG_to_LCh_D50(img: np.ndarray) -> np.ndarray:
    return loop_LCh(img, RGB_to_LCh_D50)


@nb.njit
def LCh_D50_to_IMG(lch: np.ndarray) -> np.ndarray:
    return loop_LCh(lch, LCh_D50_to_RGB)


SEXTET = (
    "255.{:03d}.000",
    "{:03d}.255.000",
    "000.255.{:03d}",
    "000.{:03d}.255",
    "{:03d}.000.255",
    "255.000.{:03d}",
)
SIX = (0, 510, 510, 1020, 1020, 1530)


def spectrum_position(n: int) -> str:
    n %= 1530
    i = n // 255
    return SEXTET[i].format(abs(n - SIX[i]))

blend_modes.py

import numba as nb
import numpy as np
from color import (
    RGB_to_HSL,
    RGB_to_HSV,
    HSL_to_RGB,
    HSV_to_RGB,
    IMG_to_LCh_D65,
    LCh_D65_to_IMG,
    IMG_to_LCh_D50,
    LCh_D50_to_IMG,
)


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_lighten(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    return np.maximum(base, top)


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_screen(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    return base + top - base * top


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_color_dodge(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    height, width = base.shape[:2]
    result = np.ones_like(base)
    for y in nb.prange(height):
        for x in nb.prange(width):
            for i in (0, 1, 2):
                t = top[y, x, i]
                if t != 1:
                    result[y, x, i] = min(1, base[y, x, i] / (1 - t))

    return result


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_linear_dodge(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    return np.minimum(1, base + top)


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_darken(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    return np.minimum(base, top)


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_multiply(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    return base * top


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_color_burn(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    height, width = base.shape[:2]
    result = np.zeros_like(base)
    for y in nb.prange(height):
        for x in nb.prange(width):
            for i in (0, 1, 2):
                t = top[y, x, i]
                if t != 0:
                    result[y, x, i] = max(0, 1 - (1 - base[y, x, i]) / t)

    return result


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_linear_burn(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    return np.maximum(0, base + top - 1.0)


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_overlay(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    height, width = base.shape[:2]
    result = np.empty_like(base)
    for y in nb.prange(height):
        for x in nb.prange(width):
            for i in (0, 1, 2):
                b = base[y, x, i]
                t = top[y, x, i]
                result[y, x, i] = (
                    2 * b * t if b < 0.5 else 2 * b + 2 * t - 2 * b * t - 1
                )

    return result


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_soft_light(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    return (1 - 2 * top) * base**2 + 2 * base * top


@nb.njit
def blend_hard_light(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    return blend_overlay(top, base)


@nb.njit(cache=True, fastmath=True)
def vivid_light(b: float, t: float) -> float:
    if t < 0.5:
        return max(0, 1 - (1 - b) / (2 * t)) if t else 0
    else:
        return min(1, b / (2 - 2 * t)) if t != 1 else 1


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_vivid_light(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    height, width = base.shape[:2]
    result = np.zeros_like(base)
    for y in nb.prange(height):
        for x in nb.prange(width):
            for i in (0, 1, 2):
                result[y, x, i] = vivid_light(base[y, x, i], top[y, x, i])

    return result


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_linear_light(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    return np.clip(base + 2 * top - 1, 0.0, 1.0)


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_pin_light(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    height, width = base.shape[:2]
    result = np.zeros_like(base)
    for y in nb.prange(height):
        for x in nb.prange(width):
            for i in (0, 1, 2):
                b = base[y, x, i]
                t = top[y, x, i]
                result[y, x, i] = min(b, 2 * t) if t < 0.5 else max(b, 2 * t - 1)

    return result


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_reflect(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    height, width = base.shape[:2]
    result = np.ones_like(base)
    for y in nb.prange(height):
        for x in nb.prange(width):
            for i in (0, 1, 2):
                t = top[y, x, i]
                if t != 1:
                    result[y, x, i] = min(1, base[y, x, i] ** 2 / (1 - t))

    return result


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_difference(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    return np.abs(base - top)


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_exclusion(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    return base + top - 2 * base * top


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_subtract(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    return np.maximum(0, base - top)


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_grain_extract(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    return np.clip(base + top - 0.5, 0, 1)


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_grain_merge(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    return np.clip(base - top + 0.5, 0, 1)


@nb.njit(cache=True, fastmath=True, parallel=True)
def blend_divide(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    height, width = base.shape[:2]
    result = np.ones_like(base)
    for y in nb.prange(height):
        for x in nb.prange(width):
            for i in (0, 1, 2):
                t = top[y, x, i]
                if t != 0:
                    result[y, x, i] = min(1, base[y, x, i] / t)

    return result


@nb.njit
def blend_HSV_Color(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    hsv = RGB_to_HSV(base)
    hsv[..., 0:2] = RGB_to_HSV(top)[..., 0:2]
    return HSV_to_RGB(hsv)


@nb.njit
def blend_HSL_Color(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    hsl = RGB_to_HSL(base)
    hsl[..., 0:2] = RGB_to_HSL(top)[..., 0:2]
    return HSL_to_RGB(hsl)


@nb.njit
def blend_color_lux(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    hsv = RGB_to_HSV(base)
    hsv[..., 0:2] = RGB_to_HSV(top)[..., 0:2]
    return HSL_to_RGB(hsv)


@nb.njit
def blend_color_nox(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    hsl = RGB_to_HSL(base)
    hsl[..., 0:2] = RGB_to_HSL(top)[..., 0:2]
    return HSV_to_RGB(hsl)


@nb.njit
def blend_color_LCh_D65(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    lch = IMG_to_LCh_D65(base)
    lch[..., 1:3] = IMG_to_LCh_D65(top)[..., 1:3]
    return LCh_D65_to_IMG(lch)


@nb.njit
def blend_color_LCh_D50(base: np.ndarray, top: np.ndarray) -> np.ndarray:
    lch = IMG_to_LCh_D50(base)
    lch[..., 1:3] = IMG_to_LCh_D50(top)[..., 1:3]
    return LCh_D50_to_IMG(lch)

theme2.py

import numpy as np
from pathlib import Path
from PIL import Image
from PyQt6.QtGui import QIcon, QImage, QPixmap
from shared2 import BLEND_MODES, FOLDER, GLOBALS, SHAPES
from typing import Callable, Tuple


STYLE = Path(f"{FOLDER}/config/style.txt").read_text().format(folder=FOLDER)


class Icon:
    def __init__(self, path: str) -> None:
        self.img = np.array(Image.open(path))
        self.height, self.width = self.img.shape[:2]
        self.base = (self.img / 255)[..., 0:3]
        self.color = np.zeros_like(self.base)
        self.set_icon()

    def set_color(self, color: Tuple[int]) -> None:
        self.color[...] = [i / 255 for i in color]

    def set_blend(self, blend: Callable) -> None:
        self.blend = blend
        self.img[..., 0:3] = (
            (blend(self.base, self.color) * 255).round().astype(np.uint8)
        )

    def set_icon(self) -> None:
        self.qimage = QImage(
            bytearray(self.img), self.width, self.height, QImage.Format.Format_RGBA8888
        )
        self.qsize = self.qimage.size()
        self.qpixmap = QPixmap(self.qimage)
        self.qicon = QIcon(self.qpixmap)


class Piece:
    def __init__(self, color: Tuple[int], blend: str, choice: str, player: str) -> None:
        GLOBALS[player] = self
        self.choices = {shape: Icon(f"{FOLDER}/icons/{shape}.png") for shape in SHAPES}
        self.color = color
        self.blend = BLEND_MODES[blend]
        self.set_active(choice)
        self.set_icon()

    def __set_blend(self) -> None:
        self.active.set_blend(self.blend)

    def set_active(self, choice: str) -> None:
        self.active = self.choices[choice]
        self.set_color()
        self.__set_blend()
        self.set_icon()

    def set_color(self) -> None:
        self.active.set_color(self.color)
        self.__set_blend()
        self.set_icon()

    def set_blend(self, blend: str) -> None:
        self.blend = BLEND_MODES[blend]
        self.__set_blend()
        self.set_icon()

    def set_icon(self) -> None:
        self.active.set_icon()

./config/style.txt

QPushButton, QPushButton#P1, QPushButton#P2, SquareButton {{
    border-style: outset;
    border-width: 3px;
    border-radius: 6px;
}}
QPushButton:pressed, QPushButton#P1:pressed, QPushButton#P2:pressed {{
    border-style: inset;
    border-width: 3px;
    border-radius: 6px;
}}
QPushButton:disabled {{
    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #000000, stop: 0.75 #808080, stop: 1 #000000);
    color: #ffffff;
    border: 3px outset #202020;
}}
QSpinBox::up-button {{
    border-width: 0px;
}}
QSpinBox::down-button {{
    border-width: 0px;
}}
QSpinBox::down-arrow:hover, QSpinBox::up-arrow:hover {{
    background: #0000ff;
}}
QSpinBox::down-arrow:pressed, QSpinBox::up-arrow:pressed {{
    background: #404080;
}}
QComboBox::drop-down {{
    border: 0px;
    padding: 0px 0px 0px 0px;
}}
QCheckBox::indicator {{
    border: 0px;
    width: 16px;
    height: 16px;
}}
QCheckBox::indicator:unchecked, QCheckBox#Base::indicator:unchecked, QCheckBox#Hover::indicator:unchecked, QCheckBox#Pressed::indicator:unchecked  {{
    image: url({folder}/icons/checkbox-unchecked.png);
}}
QCheckBox::indicator:checked, QCheckBox#Base::indicator:checked, QCheckBox#Hover::indicator:checked, QCheckBox#Pressed::indicator:checked {{
    image: url({folder}/icons/checkbox-checked.png);
}}
QCheckBox::indicator:disabled {{
    image: url({folder}/icons/checkbox-disabled.png);
}}
QComboBox::down-arrow {{
    image: url({folder}/icons/combobox-downarrow.png);
    width: 10px;
    height: 10px;
}}
QSpinBox::up-arrow, QSpinBox#Dummy::up-arrow {{
    image: url({folder}/icons/spinbox-uparrow.png);
}}
QSpinBox::down-arrow, QSpinBox#Dummy::down-arrow {{
    image: url({folder}/icons/spinbox-downarrow.png);
}}
QSpinBox::up-arrow:disabled, QSpinBox::up-arrow:off {{
    image: url({folder}/icons/spinbox-uparrow-disabled.png);
}}
QSpinBox::down-arrow:disabled, QSpinBox::down-arrow:off {{
    image: url({folder}/icons/spinbox-downarrow-disabled.png);
}}
QMainWindow, QMainWindow::title, QWidget#Window, QWidget#Window::title, QWidget#Picker, QWidget#Picker::title {{ background: #201a33; }}
QTableWidget {{
    background: #140a33;
}}
QTableWidget::item {{
    border: 3px groove #632b80;
}}
QComboBox {{
    border-radius: 6px;
    background: #422e80;
    border: 3px outset #552b80;
    color: #c000ff;
    selection-background-color: #7800d7;
    selection-color: #ffb2ff;
}}
QComboBox QAbstractItemView {{
    border-radius: 6px;
    background: #422e80;
    border: 3px groove #8000c0;
}}
QCheckBox {{
    color: #b2ffff;
}}
QCheckBox:hover, QCheckBox#Hover {{
    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #8040c0, stop: 0.75 #ff00ff, stop: 1 #8040c0);
    color: #0000ff;
}}
QCheckBox:pressed, QCheckBox#Pressed {{
    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #8040c0, stop: 0.75 #550072, stop: 1 #8040c0);
    color: #ffb2ff;
}}
QGroupBox#P1 {{
    background: #331980;
    border: 3px groove #402080;
    border-radius: 6px;
}}
QGroupBox#P2 {{
    background: #801980;
    border: 3px groove #402080;
    border-radius: 6px;
}}
QGroupBox#Stats {{
    background: #311755;
    border: 3px groove #402080;
    border-radius: 6px;
}}
QLabel {{
    color: #b2ffff;
}}
QLabel#Loss {{
    color: #ff00ff;
}}
QLabel#P1Win {{
    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #30bfbf, stop: 0.75 #b2ffff, stop: 1 #30bfbf);
    border: 3px inset #00ffff;
    border-radius: 6px;
}}
QLabel#P2Win {{
    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #bfbf30, stop: 0.75 #ffffb2, stop: 1 #bfbf30);
    border: 3px inset #ffff00;
    border-radius: 6px;
}}
QLabel#Tie {{
    color: #00ffff;
}}
QLabel#Win {{
    color: #ffff00;
}}
QLineEdit {{
    background: #422e80;
    border: 3px groove #8000c0;
    border-radius: 6px;
    color: #c000ff;
}}
QPushButton, DummyButton#Base {{
    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #5454c0, stop: 0.75 #3fbfff, stop: 1 #5454c0);
    border-color: #4080c0;
    border-radius: 6px;
    color: #ff3fff;
}}
QSpinBox {{
    background: #422e80;
    border: 3px groove #8000c0;
    border-radius: 6px;
    color: #c000ff;
}}
QTableWidget#Game::item:hover {{
    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #a33ba3, stop: 0.75 #ff5cff, stop: 1 #a33ba3);
    border: 3px outset #ff00ff;
    border-radius: 6px;
    margin: 4px;
}}
QLabel#P1, QPushButton#P1 {{
    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #6345bf, stop: 0.75 #855cff, stop: 1 #6345bf);
    border: 3px inset #4000ff;
    border-radius: 6px;
}}
QLabel#P2, QPushButton#P2 {{
    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #a36629, stop: 0.75 #ff9f40, stop: 1 #a36629);
    border: 3px inset #ff8000;
    border-radius: 6px;
}}
QPushButton:hover, QPushButton#P1:hover, QPushButton#P2:hover {{
    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #00a3a3, stop: 0.75 #5cffff, stop: 1 #00a3a3);
    border-color: #0080ff;
    border-radius: 6px;
    color: #0000ff;
}}
QPushButton:pressed, QPushButton#P1:pressed, QPushButton#P2:pressed {{
    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #00a3a3, stop: 0.75 #00b2b2, stop: 1 #00a3a3);
    border-color: #4080c0;
    border-radius: 6px;
    color: #000080;
}}
SquareButton {{
    background: #382080;
    border-color: #5319ff;
    border-radius: 6px;
}}
TitleBar {{
    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #400640, stop: 0.75 #ff19ff, stop: 1 #400640);
    border: 0px;
}}

I have posted all the code you need to run the game, the only thing missing are the icons and you can find them in the complete package found here. Because I use pickle it may be necessary to run train_ai.py once to generate the data files used by the AIs before running the game.

I really want a review because I don't know how to improve it further. How can I improve my code?

\$\endgroup\$

0

Browse other questions tagged or ask your own question.