by Kardi Teknomo
In this tutorial, we will build a simple premagic game. Through this tutorial, you will learn the following:
Premagic game is a simple solitaire board game created based on my paper "Premagic and Ideal Flow Matrices". The player would change the value of the cells such that the sum of rows is exactly the same as the sum of columns. The challenge is to win the game in minimum number of cell changes.
Clearly, the game of size N can be solved at maximum $N^{2}-N$ number of cell changes. The player would lose if the number of changes is more than the maximum number of cell change.
Simple solution to win the game is to make symmetric matrix, or constant matrix.
Let us start. First, we importing the necessary modules.
import PySimpleGUI as sg
import numpy as np
import math
Before we enter the development of the game, I need to explain several concepts that will be used in the game development:
Natural matrix is a matrix of ordered natural number. There are two ways to create natural matrix:
Given an index $i$ (1 to $mR \cdot mC$) and matrix size (i.e. maximum number of rows $mR$ or maximum number of columns $mC$), we can find row and column associated with the index.
The index start from 1 up to $mR \cdot mC$, which is the matrix size.
The formula is as follow:
$ r = r=mod(\left ( i-1 \right ),mR)+1 $
$ c = \left \lceil \frac{i}{mR} \right \rceil $
$ i = mR\cdot \left ( c-1 \right )+r $
The following codes show the natural matrix filled by columns.
# row and col based on index version 1 (index is filled by column)
mR=4 # max row
mC=6 # max col
m=np.zeros((mR,mC),dtype= np.int8)
for idx in range(1,mR*mC+1):
col=math.ceil(idx/mR)
row=((idx-1)%mR)+1
m[row-1,col-1]=idx
print(m)
The formula is as follow:
$ r = \left \lceil \frac{i}{mC} \right \rceil $
$ c = mod(\left ( i-1 \right ),mC)+1 $
$ i = mC\cdot \left ( r-1 \right )+c $
The following codes show the natural matrix filled by rows.
# row and col based on index version 2 (index is filled by row)
mR=4 # max row
mC=6 # max col
m=np.zeros((mR,mC),dtype= np.int8)
for idx in range(1,mR*mC+1):
row=math.ceil(idx/mC)
col=((idx-1)%mC)+1
m[row-1,col-1]=idx
print(m)
A matrix is called premagic if and only if the sum of rows is exactly the same of the sum of columns. This paper on "Premagic and Ideal Flow Matrices" shows an easy way to test if a matrix is premagic:
$\textbf{A}\cdot \textbf{j}=\textbf{A}^{T}\cdot \textbf{j}$
def isPremagic(A):
'''
return True if matrix A is premagic, that is the sum of rows is exactly equal to
the sum of columns
'''
(N,N1)=A.shape
j=np.ones((N,1))
sR=np.dot(A,j)
sC=np.dot(A.transpose(),j)
return np.array_equal(sR,sC), sR, sC
# test it
R=np.random.randint(5,size=(5,5))
A=np.dot(R.transpose(),R) # create symmetric matrix, which is surely premagic (see paper by Teknomo)
isPremagic(A)
# version 1
def generateBoardLayout(N=2):
# produce board layout
s="["
for row in range(1,N+2):
s=s+"["
for col in range(1,N+1):
idx=(N)*(row-1)+col # index of natural matrix filled by rows
if row<=N:
element="sg.InputText('e"+str(idx)+"',size=(6,1),background_color = 'yellow')"
else: # row = N + 1
element="sg.InputText('c"+str(col)+"',size=(6,1))"
if col==N: # last column
if row==N+1: # last column, last row
s=s+element+",sg.InputText('sum',size=(6,1))]"
else: # last column, not last row
s=s+element+",sg.InputText('r"+str(row)+"',size=(6,1))],"
else: # not last column
s=s+element+","
# if row==N: # last row
s=s+"]"
return s
generateBoardLayout(3)
In the code below, we will use the layout function to generate the layout programmatically and then show.
# fist version
def premagicGame(N=2):
sg.ChangeLookAndFeel('SystemDefault')
layout=generateBoardLayout(N)
window = sg.Window("Premagic Game", eval(layout),
finalize=True,
grab_anywhere=False,
return_keyboard_events=True,
use_default_focus=False, location=(50, 50))
while True:
event, value = window.read()
if event == sg.WIN_CLOSED:
break
# for debugging event and value
print('Event {}'.format(event))
print('Value {}'.format(value))
window.close()
premagicGame(N=3)
The result of the first version layout would be shown below.
In the second version of layout, we will change the layout to accept event (by setting enable_events=True) and we add key such that we can call them uniquely later.
The following layout is copy of first version layout, add enable_events=True in the entry input text and then add key to all elements.
# version 2
def generateBoardLayout(N=2):
# produce board layout
s="["
for row in range(1,N+2):
s=s+"["
for col in range(1,N+1):
idx=(N)*(row-1)+col # index of natural matrix filled by rows
if row<=N:
element="sg.InputText('',key='txtE"+str(idx)+"',size=(6,1),background_color='yellow',enable_events=True)"
else: # row = N + 1
element="sg.InputText('',key='txtC"+str(col)+"',size=(6,1))"
if col==N: # last column
if row==N+1: # last column, last row
s=s+element+",sg.InputText('',key='txtAll',size=(6,1))]"
else: # last column, not last row
s=s+element+",sg.InputText('',key='txtR"+str(row)+"',size=(6,1))],"
else: # not last column
s=s+element+","
# if row==N: # last row
s=s+"]"
return s
generateBoardLayout(5)
In the second version of the game program, we copy the first version of the game above and then add more code to initialize and in the while loop section.
# second version
def premagicGame(N=2):
sg.ChangeLookAndFeel('SystemDefault')
layout=generateBoardLayout(N)
window = sg.Window("Premagic Game", eval(layout),
finalize=True,
grab_anywhere=False,
return_keyboard_events=True,
use_default_focus=False, location=(50, 50))
# initialize
A=np.random.randint(low=0, high=9, size=(N,N)) # create random matrix N by N
for idx in range(1,N*N+1):
row=math.ceil(idx/N) # row of natural matrix
col=((idx-1)%N)+1 # col of natural matrix
window['txtE'+str(idx)].update(A[row-1,col-1]) # fill textEntry with element of A
retVal,sR,sC=isPremagic(A) # isPremagic, sum of row, sum of column
sumAll=np.sum(sC) # sum of all
for row in range(1,N+1): # fill up the sum of row
window['txtR'+str(row)].update(int(sR[row-1][0]))
for col in range(1,N+1): # fill up the sum of column
window['txtC'+str(col)].update(int(sC[col-1][0]))
window['txtAll'].update(int(sumAll)) # fill up the sum of all
count=0
maxCount=N*N-N
while True:
event, value = window.read()
if event == sg.WIN_CLOSED:
break
if event[:4]=="txtE": #text Entry
try:
val=value[event] # get the text value of the entry
idx=int(event.split("txtE",1)[1]) # get the index from the key of the entry
row=math.ceil(idx/N) # get row of the entry
col=((idx-1)%N)+1 # get col of the entry
window[event].update(val) # update value of entry
A[row-1,col-1]=val # update the matrix
# update sum of rows, sum of columns, and total
retVal,sR,sC=isPremagic(A)
sumAll=np.sum(sC)
for row in range(1,N+1):
window['txtR'+str(row)].update(int(sR[row-1][0]))
for col in range(1,N+1):
window['txtC'+str(col)].update(int(sC[col-1][0]))
window['txtAll'].update(int(sumAll))
count=count+1 # update number of changes by player
window.TKroot.title(str(maxCount-count)+" more steps") # remind player
# check if reach correct answer of premagic
if retVal:
sg.popup("You have won in " + str(count) + " changes")
break
# the game must be able to be solved in less than max number of changes
if count>maxCount:
sg.popup("You loss. You changed the cells " + str(count) + " times. The maximum change is "+str(maxCount))
break
except Exception as e:
# print('error=',e)
pass
# for debugging event and value
# print('Event {}'.format(event))
# print('Value {}'.format(value))
window.close()
premagicGame(N=5)
The result of the second version would be similar to the picture below.
That's all. Enjoy the game and extend the game with your own additional optional features.
Visit people.Revoledu.com for more tutorials in Data Science
Copyright © 2021-2023 Kardi Teknomo
Permission is granted to share this notebook as long as the copyright notice is intact.