Spectrum-Environment for Pygame

Show us what you're working on, (preferably with screenshots).
Post Reply
FFoulkes
Microbot
Posts: 161
Joined: Thu Feb 07, 2019 2:42 pm

Re: Spectrum-Environment for Pygame

Post by FFoulkes »

Hi guys,

a Spectrum emulator in Python would probably be too slow, I think. Tried one written in Perl once. It kind of worked, but it wasn't much fun compared to programs like "fuse".

Pygame gave me a hard time in the meantime. I coded some "Space Invaders"-clone in the Spectrum resolution (because the pixel artwork could be found as images somewhere), but I found, somebody else did it all way better than me, so I'm trying to learn from his code at the moment. It may still take some time, but there's some progress.

Here's something, I can already show you (I use Python 2.7 and Pygame 1.9.1, so there may be problems on other configurations). Anyway, here it goes ('q' left, 'w' right, "right shift" jump, 'a' end). Hope you like it :). To get the movement accurate, is actually quite tricky. I'd say, I'm about 90% there at the moment:

Code: Select all

#!/usr/bin/python
# coding: utf-8

import pygame
from pygame.locals import *

# License for script code: GNU GPL, v.2.

import os

SCALEFACTOR = 3

class ZXEnvironment:

    def __init__(self):
        self.zx_paperwidth  = 256
        self.zx_paperheight = 176
        self.zx_char_width  = self.zx_paperwidth  / 8 # 32
        self.zx_char_height = self.zx_paperheight / 8 # 22
        self.pc_paperwidth  = self.zx_paperwidth  * SCALEFACTOR
        self.pc_paperheight = self.zx_paperheight * SCALEFACTOR
        self.zx_border = 16 # 2
        self.pc_border  = self.zx_border * SCALEFACTOR
        self.screenwidth  = (self.zx_paperwidth + 2 * self.zx_border) * SCALEFACTOR
        self.screenheight = (self.zx_paperheight + 2 * self.zx_border) * SCALEFACTOR
        self.data = ZXData()
        self.colours = self.data.colours

    def initPaper(self, screen):
        self.screen = screen
        self.paper = pygame.Surface((self.pc_paperwidth, self.pc_paperheight))
        self.paperpos = (self.pc_border, self.pc_border)
        self.cls()

    def cls(self, do_blit = 1, colourname = "white"):
        self.paper.fill(self.colours[colourname])
        self.papercolourname = colourname
        if do_blit:
            self.screen.blit(self.paper, self.paperpos)

    def border(self, colourname):
        bars = (pygame.Surface((self.screenwidth, self.pc_border)),
                pygame.Surface((self.pc_border, self.pc_paperheight)))
        pos = { 0 : ((0, 0), (0, self.pc_border + self.pc_paperheight)),
                1 : ((0, self.pc_border),
                     (self.pc_border + self.pc_paperwidth, self.pc_border))}
        for i in range(len(bars)):
            bars[i].fill(self.colours[colourname])
            for u in pos[i]:
                self.screen.blit(bars[i], u)
        self.bordercolourname = colourname

    def zxToPCxy(self, zx_x, zx_y):
        x = zx_x * SCALEFACTOR
        y = zx_y * SCALEFACTOR
        return (x, y)

class Main:

    def __init__(self):

        os.environ['SDL_VIDEO_WINDOW_POS'] = "218, 5"
        self.zxenv = ZXEnvironment()
        pygame.init()
        self.screen = pygame.display.set_mode((self.zxenv.screenwidth, self.zxenv.screenheight))
        pygame.display.set_caption('Hello Willy!')
        self.keys = None
        self.zxenv.initPaper(self.screen)
        self.zxenv.cls(1, "black")
        self.bordercolour = "red"
        self.zxenv.border(self.bordercolour)
        self.initSprites()
        self.run()

    def run(self):
        while True:
            self.clock = pygame.time.Clock()
            self.clock.tick(10)
            self.zxenv.cls(do_blit = 0, colourname = self.zxenv.papercolourname)
            r = self.processEvents()
            if r == "quit":
                return r
            self.mw.update(self.zxenv.paper, self.keys)

            self.screen.blit(self.zxenv.paper, self.zxenv.paperpos)
            pygame.display.flip()
            
    def processEvents(self):

        self.keys = []
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                return "quit"
            if event.type == pygame.KEYDOWN:
                self.keys.append(event.key)
        self.keys = pygame.key.get_pressed()
        if self.keys[K_a]:
            pygame.quit()
            return "quit"
        return 0

    def initSprites(self):
        self.mw = Willy("Willy", 30, 80, self.zxenv, "white")


class UDGSprite(pygame.sprite.Sprite):

    def __init__(self, name, zx_x, zx_y, zxenv, inkcolourname):
        pygame.sprite.Sprite.__init__(self)
        self.name = name
        self.zxenv = zxenv
        self.zx_position = (zx_x, zx_y)
        self.pc_position = self.zxenv.zxToPCxy(zx_x, zx_y)
        self.colours = self.zxenv.colours
        self.inkcolourname = inkcolourname
        self.data = SpriteData()
        self.imagedata = self.data.getImagedata(self.name)
        self.images = []
        self.zx_imagesizes = []
        self.pc_imagesizes = []
        self.createImages()
        self.currentimage = 0
        self.image = None
        self.rect  = None
        self.active = False
        self.xmoved = 0
        self.ymoved = 0

    def setImage(self, imagenumber):
        self.currentimage = imagenumber
        self.image = self.images[imagenumber]
        self.rect = pygame.Rect(self.pc_position,
                                self.pc_imagesizes[imagenumber])

    def zx_moveTo(self, zx_x, zx_y):
        self.zx_position = (zx_x, zx_y)
        self.pc_position = self.zxenv.zxToPCxy(zx_x, zx_y)
        self.rect = pygame.Rect(self.pc_position,
                                self.pc_imagesizes[self.currentimage])

    def zx_moveBy(self, move_zx_x, move_zx_y):
        self.zx_position = (self.zx_position[0] + move_zx_x,
                            self.zx_position[1] + move_zx_y)
 
        self.pc_position = (self.pc_position[0] + move_zx_x * SCALEFACTOR,
                            self.pc_position[1] + move_zx_y * SCALEFACTOR)
        self.rect = pygame.Rect(self.pc_position,
                                self.pc_imagesizes[self.currentimage])

    def createImages(self):
        a = self.imagedata.keys()
        a.sort()
        for i in a:
            b = i.split("_")
            if len(b) == 1:
                num = 0
            else:
                num = int(b[1])
            b = []
            numstr = "{0:0" + str(self.imagedata[i][0]) + "b}"
            for u in self.imagedata[i][1]:
                binstring = numstr.format(u)
                b.append(binstring)
            zx_spritesize = (len(b[0]), len(b))
            pc_spritesize = (zx_spritesize[0] * SCALEFACTOR,
                             zx_spritesize[1] * SCALEFACTOR)
            surface = pygame.Surface(pc_spritesize)
            surface = pygame.Surface.convert_alpha(surface)
            surface = self.plotImage(surface, b, zx_spritesize[0], self.inkcolourname)
            self.images.append(surface)
            self.zx_imagesizes.append(zx_spritesize)
            self.pc_imagesizes.append(pc_spritesize)

    def plotImage(self, surface, data_, spritewidth, inkcolourname):
        pxarray = pygame.PixelArray(surface)
        t_x = 0
        t_y = 0
        for line in data_:
            for bit in line:
                if bit == "1":
                    # Plot one ZX pixel:
                    for pixelline in range(SCALEFACTOR):
                        for pixelrow in range(SCALEFACTOR):
                            pxarray[t_x * SCALEFACTOR + pixelline][t_y * SCALEFACTOR + pixelrow] = self.colours[inkcolourname]
                else:
                    # Plot one ZX pixel:
                    for pixelline in range(SCALEFACTOR):
                        for pixelrow in range(SCALEFACTOR):
                            pxarray[t_x * SCALEFACTOR + pixelline][t_y * SCALEFACTOR + pixelrow] = (0, 0, 0, 0)

                t_x += 1
            # Next line (like a typewriter):
            t_y += 1
            t_x -= spritewidth
        del pxarray
        return surface

class Willy(UDGSprite):

    def __init__(self, name, zx_x, zx_y, zxenv, inkcolourname):
        UDGSprite.__init__(self, name, zx_x, zx_y, zxenv, inkcolourname)
        #  Willy's movement is actually quite strange:
        self.imagepos = (0, 2, 0, 1, 3, 4, 3, 5)
        self.moves    = (3, 3, 1, 1, 3, 3, 1, 1)
        self.ind = 0
        self.lifted = (-3, -4, -8, -4, -3, 0, 0, 0, 0, 3, 4, 8, 4, 3)
        self.jumping = False
        self.lindex = 0
        self.direction = 0

    def update(self, surface, keys, *args):
        self.setImage(self.imagepos[self.ind])
        if self.jumping:
            self.jump()
            surface.blit(self.image, self.pc_position)
            return
        if keys[K_q]: 
            self.direction = -1
            self.go_left()
        if keys[K_w]:
            self.direction = 1
            self.go_right()
        if not keys[K_q] and not keys[K_w]:
            self.direction = 0
        if keys[K_RSHIFT]:
            self.jumping = True
        surface.blit(self.image, self.pc_position)

    def go_left(self):
        if self.ind < 4:
            self.ind += 4
        if self.zx_position[0] > 0:
            self.zx_moveBy(- self.moves[self.ind], 0)
            self.ind += 1
            if self.ind > 7:
                self.ind = 4

    def go_right(self):
        if self.ind > 3:
            self.ind -= 4
        if self.zx_position[0] < self.zxenv.zx_paperwidth - self.zx_imagesizes[self.currentimage][0]:
            self.zx_moveBy(self.moves[self.ind], 0)
            self.ind += 1
            if self.ind > 3:
                self.ind = 0

    def jump(self):
        if self.direction == -1:
            self.go_left()
        elif self.direction == 1:
            self.go_right()
        self.zx_moveBy(0, self.lifted[self.lindex])
        self.lindex += 1
        if self.lindex > len(self.lifted) - 1:
            self.lindex = 0
            self.jumping = 0

class ZXData:

    def __init__(self):

        self.colours = {"black"   : (0, 0, 0),
                        "blue"    : (0, 0, 197),
                        "red"     : (189, 0, 0),
                        "magenta" : (189, 0, 197),
                        "green"   : (0, 190, 0),
                        "cyan"    : (0, 190, 197),
                        "yellow"  : (189, 190, 0),
                        "white"   : (189, 190, 197)}

class SpriteData:

    def __init__(self):
        self.imagedata = {'Willy_0' : (8, (6, 62, 124, 52, 62, 60, 24, 60, 126, 126, 247, 251, 60, 118, 110, 119)),
                          'Willy_1' : (10, (12, 124, 248, 104, 124, 120, 48, 120, 252, 510, 1023, 891, 124, 237, 391, 454)),
                          'Willy_2' : (6, (3, 31, 62, 26, 31, 30, 12, 30, 55, 55, 55, 59, 30, 12, 12, 14)),
                          'Willy_3' : (8, (96, 124, 62, 44, 124, 60, 24, 60, 126, 126, 239, 223, 60, 110, 118, 238)),
                          'Willy_4' : (10, (192, 248, 124, 88, 248, 120, 48, 120, 252, 510, 1023, 891, 248, 732, 902, 398)),
                          'Willy_5' : (6, (48, 62, 31, 22, 62, 30, 12, 30, 59, 59, 59, 55, 30, 12, 12, 28))}


    def getImagedata(self, spritename):
        imagedataslice = {}
        for i in self.imagedata.keys():
            if i.startswith(spritename):
                imagedataslice[i] = self.imagedata[i]
        return imagedataslice

if __name__ == '__main__':
    Main()
In the end, you will need an idea for an interesting game and also the skill to create the artwork for it.
I'm not sure, if I have the will to go through all of that. Back then, they could at least put on a tape, what they had created, and sell it. :lol:
FFoulkes
Microbot
Posts: 161
Joined: Thu Feb 07, 2019 2:42 pm

Re: Spectrum-Environment for Pygame

Post by FFoulkes »

Hmm, no one tried it. Oh well.

I just realized, I can't directly use a "PRINT AT" in Pygame. Because the screen is updated every frame. It is cleared and reprinted many times per second. It wouldn't be good, to reprint the data that often.
It's better to create the writing once on a Pygame Surface, and then reblit that surface each frame. I call that a "StringSprite". It's the same result, but a bit more complicated than such a simple command like in the old Basic.
It's pretty cool though, that in Pygame you can create a transparent surface, that holds just the "INK" and uses, whatever "PAPER" is below it.

Oh, this may be interesting for you: In my example, Willy couldn't be kept inside the screen sometimes, because the graphics have different widths (8, 10, 6 Bit). When I looked at the original game, there's a small additional border on both sides of the screen. Probably to solve that problem. This border is also carefully and beautifully designed, with different colours and patterns in each level. Nice.
FFoulkes
Microbot
Posts: 161
Joined: Thu Feb 07, 2019 2:42 pm

Re: Spectrum-Environment for Pygame

Post by FFoulkes »

Heya,

still was impressed by the Z80 assembly tutorial on Youtube by Darryl Sloan. He's demonstrating "Connect 4", using a single UDG in different colours:

Z80 assembly Youtube-tutorial by Darryl Sloan

I wondered, if I could do that in Pygame. It seems, I could. Here's my result.
Keys are: "q" (or "o") - left, "w" (or "p") right, "space" - drop, "u" - undo move, "r" - restart game:

Code: Select all

#!/usr/bin/python
# coding: utf-8

import pygame
from pygame.locals import *

import os
import zlib
import base64

""" winwithfour.py, 1.0:

    A little game in Python/Pygame (inspired by a very good Z80 Assembly
    tutorial on Youtube).

    Copyright 2019, Forum-name: Major Percival FFoulkes, GNU GPL v.2,

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""

SCALEFACTOR = 3
FPS         = 50
GRIDWIDTH   = 7
GRIDHEIGHT  = 6
MATCHES     = 4

class ZXEnvironment:

    def __init__(self):
        self.zx_paperwidth  = 256
        self.zx_paperheight = 176
        self.zx_char_width  = self.zx_paperwidth  / 8 # 32
        self.zx_char_height = self.zx_paperheight / 8 # 22
        self.pc_paperwidth  = self.zx_paperwidth  * SCALEFACTOR
        self.pc_paperheight = self.zx_paperheight * SCALEFACTOR
        self.zx_border = 16 # 2
        self.pc_border = self.zx_border * SCALEFACTOR
        self.screenwidth  = (self.zx_paperwidth  + 2 * self.zx_border) * SCALEFACTOR
        self.screenheight = (self.zx_paperheight + 2 * self.zx_border) * SCALEFACTOR
        self.data = Data()
        self.colours = self.data.colours

    def initBorderAndPaper(self, screen):
        self.screen = screen
        self.paper = pygame.Surface((self.pc_paperwidth, self.pc_paperheight))
        self.paperrect = self.paper.get_rect(topleft = (self.pc_border, self.pc_border))
        self.borderbars = (pygame.Surface((self.pc_border, self.screenheight)),
                           pygame.Surface((self.pc_paperwidth, self.pc_border)))
        self.paperborder = pygame.Surface((self.pc_paperwidth, self.pc_border))
        self.paperborderrect = self.paperborder.get_rect(topleft = (self.pc_border, self.pc_border + self.pc_paperheight))
        self.borderpositions = {0 : ((0, 0), (self.pc_border + self.pc_paperwidth, 0)),
                                1 : ((self.pc_border, 0),)}

    def cls(self, colourname):
        self.setPaperColour(colourname)
        self.blitPaperToScreen()

    def border(self, colourname):
        self.setBorderColour(colourname)
        self.blitBorderToScreen()

    def setBorderColour(self, colourname):
        for b in self.borderbars:
            b.fill(self.colours[colourname])
        self.paperborder.fill(self.colours[colourname])
        self.bordercolourname = colourname

    def setPaperColour(self, colourname):
        self.paper.fill(self.colours[colourname])
        self.papercolourname = colourname

    def blitPaperToScreen(self):
        """ If you drawed on the paper, you'd have to call "fill()"
            to get it clean again. Instead, you blit the (clean) paper
            and keep drawing on top of it onto the screen. """
        self.screen.blit(self.paper, self.paperrect)

    def blitPaperborderToScreen(self):
        self.screen.blit(self.paperborder, self.paperborderrect)

    def blitBorderToScreen(self):
        for i in range(len(self.borderbars)):
            for u in self.borderpositions[i]:
                self.screen.blit(self.borderbars[i], u)
        self.screen.blit(self.paperborder, self.paperborderrect)

    def zxToPCxy(self, zx_x, zx_y):
        x = self.pc_border + zx_x * SCALEFACTOR
        y = self.pc_border + zx_y * SCALEFACTOR
        return (x, y)


class StringSprite(pygame.sprite.Sprite):

    def __init__(self, zx_x, zx_y, zxenv, image):
        pygame.sprite.Sprite.__init__(self)
        self.zxenv = zxenv
        self.image = image
        self.rect  = self.image.get_rect()
        self.rect.topleft = self.zxenv.zxToPCxy(zx_x, zx_y)

    def draw(self, surface):
        surface.blit(self.image, self.rect)


class UDGSprite(pygame.sprite.Sprite):

    def __init__(self, name, zx_x, zx_y, zxenv, colourname):
        pygame.sprite.Sprite.__init__(self)
        self.name = name
        self.zxenv = zxenv
        self.zx_position = (zx_x, zx_y)
        self.pc_position = self.zxenv.zxToPCxy(zx_x, zx_y)
        self.colourname = colourname
        self.colour = self.zxenv.colours[colourname]
        self.transparent = (0, 0, 0, 0)
        self.data = Data()
        self.images = []
        self.zx_imagesizes = []
        self.pc_imagesizes = []
        self.currentimage = 0
        self.image = None
        self.rect  = None
        self.createImages()

    def setImage(self, imagenumber):
        self.currentimage = imagenumber
        self.image = self.images[imagenumber]
        self.rect = pygame.Rect(self.pc_position,
                                self.pc_imagesizes[imagenumber])

    def moveTo(self, zx_x, zx_y):
        self.zx_position = (zx_x, zx_y)
        self.pc_position = self.zxenv.zxToPCxy(zx_x, zx_y)
        self.rect = pygame.Rect(self.pc_position,
                                self.pc_imagesizes[self.currentimage])

    def moveBy(self, move_zx_x, move_zx_y):
        self.zx_position = (self.zx_position[0] + move_zx_x,
                            self.zx_position[1] + move_zx_y)
        self.pc_position = (self.pc_position[0] + move_zx_x * SCALEFACTOR,
                            self.pc_position[1] + move_zx_y * SCALEFACTOR)
        self.rect = pygame.Rect(self.pc_position,
                                self.pc_imagesizes[self.currentimage])

    def draw(self, surface):
        surface.blit(self.image, self.pc_position)

    def createImages(self):
        # Format expected by imagedata: {'UDG1_0': (8, (1, 2, 3, 4)),
        #                                'UDG1_1': (8, (5, 6, 7, 8))}
        imagedata = self.data.getImagedata(self.name)
        a = imagedata.keys()
        a.sort()
        for i in a:
            b = []
            numstr = "{0:0" + str(imagedata[i][0]) + "b}"
            for u in imagedata[i][1]:
                binstring = numstr.format(u)
                b.append(binstring)
            self.addImage(b, self.colourname)

    def addImage(self, binstringlist, colourname):
        zx_spritesize = (len(binstringlist[0]), len(binstringlist))
        pc_spritesize = (zx_spritesize[0] * SCALEFACTOR,
                         zx_spritesize[1] * SCALEFACTOR)
        surface = pygame.Surface(pc_spritesize)
        surface = surface.convert_alpha(surface)
        surface = self.plotImage(surface, binstringlist, zx_spritesize[0], colourname)
        self.images.append(surface)
        self.zx_imagesizes.append(zx_spritesize)
        self.pc_imagesizes.append(pc_spritesize)
        
    def plotImage(self, surface, data_, spritewidth, colourname):
        pxarray = pygame.PixelArray(surface)
        t_x = 0
        t_y = 0
        for line in data_:
            for bit in line:
                if bit == "1":
                    plotcolour = self.zxenv.colours[colourname]
                else:
                    plotcolour = self.transparent
                # Plot one ZX pixel:
                for pixelline in range(SCALEFACTOR):
                    for pixelrow in range(SCALEFACTOR):
                        pxarray[t_x * SCALEFACTOR + pixelline][t_y * SCALEFACTOR + pixelrow] = plotcolour
                t_x += 1
            # Next line (like a typewriter):
            t_y += 1
            t_x -= spritewidth
        del pxarray
        return surface


class GridSprite(UDGSprite):

    def __init__(self, name, zx_x, zx_y, zxenv, colourname):
        self.zx_x = zx_x
        self.zx_y = zx_y
        UDGSprite.__init__(self, name, zx_x, zx_y, zxenv, colourname)

    def createImages(self):
        imagedata = self.data.getImagedata(self.name)
        b = []
        for i in range(GRIDHEIGHT):
            for u in imagedata:  
                bstr = "{0:08b}".format(u)
                bstr *= GRIDWIDTH
                b.append(bstr)
        self.addImage(b, "blue")


class Tile(UDGSprite):

    def __init__(self, name, zx_x, zx_y, zxenv, colourname, grid_x, grid_y, gridzero_y):
        UDGSprite.__init__(self, name, zx_x, zx_y, zxenv, colourname)
        self.colourname = colourname
        self.grid_x     = grid_x
        self.grid_y     = grid_y
        self.gridzero_y = gridzero_y
        self.active     = False
        self.moveable   = False
        self.falling    = 0

    def setMoveable(self, zx_to_x, zx_to_y):
        self.active = True
        self.moveable = True
        self.moveTo(zx_to_x, zx_to_y)
        self.grid_x = 3
        self.grid_y = -1

    def setImmoveable(self):
        self.moveable = False

    def setActive(self):
        self.active = True

    def setInactive(self):
        self.active = False

    def createImages(self):
        imagedata = self.data.getImagedata(self.name)
        b = []
        for i in imagedata:  
            bstr = "{0:08b}".format(i)
            b.append(bstr)
        self.addImage(b, self.colourname)

    def moveSideways(self, direction):
        if not self.moveable:
            return
        if direction == "left":
            if self.grid_x > 0:
                self.grid_x -= 1
                self.moveBy(-8, 0)
        if direction == "right":
            if self.grid_x < GRIDWIDTH - 1:
                self.grid_x += 1
                self.moveBy(8, 0)

    def setTimer(self):
        self.timer = pygame.time.get_ticks()

    def update(self):
        if self.moveable and self.falling == 1: 
            zx_ydest = self.gridzero_y + self.grid_y * 8
            if self.zx_position[1] < zx_ydest:
                self.moveBy(0, 1)
            else:
                self.falling = 2
                self.setImmoveable()

    def draw(self, surface):
        if self.active:
            surface.blit(self.image, self.pc_position)


class WinTile(UDGSprite):

    def __init__(self, name, zx_x, zx_y, zxenv, colourname, grid_x, grid_y):
        self.wincolour = colourname
        UDGSprite.__init__(self, name, zx_x, zx_y, zxenv, colourname)
        self.grid_x = grid_x
        self.grid_y = grid_y
        self.active = False
        self.delaytime = 600
        self.timer = pygame.time.get_ticks()

    def setActive(self):
        self.active = True
        self.timer = pygame.time.get_ticks()

    def setInactive(self):
        self.active = False

    def setWinColour(self, colourname):
        self.wincolour = colourname
        self.images = []
        self.zx_imagesizes = []
        self.pc_imagesizes = []
        self.createImages()

    def createImages(self):
        imagedata = self.data.getImagedata(self.name)
        b = []
        for i in imagedata:  
            bstr = "{0:08b}".format(i)
            b.append(bstr)
        self.addImage(b, "magenta")
        self.addImage(b, self.wincolour)

    def update(self, currenttime):
        if not self.active:
            return
        if currenttime - self.timer > self.delaytime:
            self.setImage(1 - self.currentimage)
            self.timer += self.delaytime

    def draw(self, surface):
        if self.active:
            surface.blit(self.image, self.pc_position)


class Main:

    def __init__(self):

        self.zxenv = ZXEnvironment()
        os.environ['SDL_VIDEO_WINDOW_POS'] = "218, 5"
        pygame.init()
        self.screen = pygame.display.set_mode((self.zxenv.screenwidth, self.zxenv.screenheight))
        self.clock = pygame.time.Clock()
        pygame.display.set_caption('Win with Four')
        self.zxenv.initBorderAndPaper(self.screen)
        self.data = Data()
        self.combinations = self.initCombinations()
        self.initGame()

        # Main loop:
        while True:
            self.clock.tick(FPS)
            self.timer = pygame.time.get_ticks()
            r = self.processEvents()
            if r == "quit":
                pygame.quit()
                return
            if r == "replay":
                self.initGame()
                continue
            self.zxenv.blitPaperToScreen()
            self.drawMessages("headline")
            for s in self.activetiles:
                s.update()
            for s in self.redtiles:
                s.draw(self.zxenv.screen)
            for s in self.yellowtiles:
                s.draw(self.zxenv.screen)
            self.gridSprite.draw(self.zxenv.screen)
            if self.activetile.falling == 2:
                if self.won:
                    for s in self.wintiles:
                        s.update(self.timer)
                        s.draw(self.zxenv.screen)
                    if self.won == "red":
                        self.drawMessages("redwin", "replay")
                    if self.won == "yellow":
                        self.drawMessages("yellowwin", "replay")
                elif self.outofmoves:
                    self.drawMessages("tie", "replay")
                else:
                    self.nextTile()
            pygame.display.flip()

    def initGame(self):
        self.moves = 0
        self.outofmoves = False
        self.won = None
        self.initSprites()
        self.initGrid()
        self.zxenv.cls("white")
        self.zxenv.border("red")
        for s in self.wintiles:
            s.setInactive()

    def dropTile(self):
        # Selected column is already full:
        if self.grid[self.activetile.grid_x][0]:
            return
        self.activetile.grid_y = 0
        while self.activetile.grid_y < GRIDHEIGHT and not self.grid[self.activetile.grid_x][self.activetile.grid_y]:
            self.activetile.grid_y += 1
        self.activetile.grid_y -= 1
        self.activetile.falling = 1
        self.activetiles.append(self.activetile)
        if self.activetile.colourname == "red":
            self.grid[self.activetile.grid_x][self.activetile.grid_y] = 1
        if self.activetile.colourname == "yellow":
            self.grid[self.activetile.grid_x][self.activetile.grid_y] = 2
        self.moves += 1
        if self.moves == GRIDWIDTH * GRIDHEIGHT:
            self.outofmoves = True
            return
        # Clever: Don't check every frame, but only, when a tile is dropped:
        self.checkGameWon()
        if self.won:
            return

    def undoMove(self):
        if self.moves > 0:
            self.moves -= 1
            self.activetile.setImmoveable()
            self.activetile.setInactive()
            if self.activetile.colourname == "red":
                self.redindex -= 1
            if self.activetile.colourname == "yellow":
                self.yellowindex -= 1
            undotile = self.activetiles[len(self.activetiles) - 1]
            undotile.setInactive()
            self.grid[undotile.grid_x][undotile.grid_y] = 0
            self.activetiles.pop()
            self.nextTile()

    def nextTile(self):
        if self.moves % 2:
            self.yellowindex += 1
            self.activetile = self.yellowtiles[self.yellowindex]
            self.activetile.setMoveable(self.gridSprite.zx_x + 3 * 8, self.gridSprite.zx_y - 8)
        else:
            self.redindex += 1
            self.activetile = self.redtiles[self.redindex]
            self.activetile.setMoveable(self.gridSprite.zx_x + 3 * 8, self.gridSprite.zx_y - 8)

    def checkGameWon(self):
        if self.won or self.outofmoves:
            return
        for i in self.combinations:
            red = 0
            yellow = 0
            for u in i:
                value_at_pos = self.grid[int(u[0])][int(u[1])]
                if value_at_pos == 1:
                    red += 1
                if value_at_pos == 2:
                    yellow += 1
            if red == MATCHES:
                self.won = "red"
                wincoords = i
                break
            if yellow == MATCHES:
                self.won = "yellow"
                wincoords = i
                break
        if self.won:
            # At this point, we already know, that someone has won the game,
            # but the main loop still has to wait for the last tile to fall:
            self.setWinTilesCoords(wincoords)

    def setWinTilesCoords(self, coords):
        for i in range(len(coords)):
            self.wintiles[i].setWinColour(self.won)
            self.wintiles[i].moveTo(self.gridSprite.zx_x + 8 * int(coords[i][0]),
                               self.gridSprite.zx_y + 8 * int(coords[i][1]))
            self.wintiles[i].setActive()

    def drawMessages(self, *msgnames):
        for i in msgnames:
            self.messages[i].draw(self.zxenv.screen)

    def initCombinations(self):
        return self.data.getAllMatchesInAGridCombinations(GRIDWIDTH, GRIDHEIGHT, MATCHES)

    def initGrid(self):
        self.grid = []
        for column in range(GRIDWIDTH):
            a = []
            for row in range(GRIDHEIGHT):
                a.append(0)
            self.grid.append(a)

    def initSprites(self):
        self.gridSprite = GridSprite("Grid", 96, 48, self.zxenv, "blue")
        self.gridSprite.setImage(0)
        msgpos = {"headline"  : (-24, -24),
                  "redwin"    : (-16, 72),
                  "yellowwin" : (-24, 72),
                  "tie"       : (-72, 72),
                  "replay"    : (-64, 88)}
        self.messages = {}
        stringimages = self.data.getStringImages()
        for i in msgpos.keys():
            self.messages[i] = StringSprite(self.gridSprite.zx_x + msgpos[i][0], self.gridSprite.zx_y + msgpos[i][1], self.zxenv, stringimages[i])
        n = GRIDWIDTH * GRIDHEIGHT // 2
        # A few more tiles than needed (just in case :) ):
        n += 5
        self.redtiles = []
        self.yellowtiles = []
        for i in range(n):
            tr = Tile("Tile", 0, 0, self.zxenv, "red", -1, -1, self.gridSprite.zx_y)
            tr.setImage(0)
            self.redtiles.append(tr)
            ty = Tile("Tile", 0, 0, self.zxenv, "yellow", -1, -1, self.gridSprite.zx_y)
            ty.setImage(0)
            self.yellowtiles.append(ty)
        self.wintiles = []
        for i in range(MATCHES):
            t = WinTile("Tile", 0, 0, self.zxenv, "magenta", -1, -1)
            t.setImage(0)
            self.wintiles.append(t)
        self.redindex = 0
        self.yellowindex = 0
        self.activetile = self.redtiles[self.redindex]
        self.activetile.setMoveable(self.gridSprite.zx_x + 3 * 8, self.gridSprite.zx_y - 8)
        self.activetiles = [self.activetile]

    def processEvents(self):

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return "quit"
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_e or event.key == pygame.K_a:
                    return "quit"
                if event.key == pygame.K_r:
                    return "replay"
                if not self.won and not self.outofmoves and self.activetile.falling != 1:
                    if event.key == pygame.K_q or event.key == pygame.K_o:
                        self.activetile.moveSideways("left")
                    if event.key == pygame.K_w or event.key == pygame.K_p:
                        self.activetile.moveSideways("right")
                    if event.key == pygame.K_u:
                        self.undoMove()
                    if event.key == pygame.K_SPACE:
                        self.dropTile()
        return 0


class Data:

    def __init__(self):

        self.colours = {"black"   : (0, 0, 0),
                        "blue"    : (0, 0, 197),
                        "red"     : (189, 0, 0),
                        "magenta" : (189, 0, 197),
                        "green"   : (0, 190, 0),
                        "cyan"    : (0, 190, 197),
                        "yellow"  : (189, 190, 0),
                        "white"   : (189, 190, 197)}

        self.gridSpriteData = (255, 231, 195, 129, 129, 195, 231, 255)
        self.tiledata       = (0, 24, 60, 126, 126, 60, 24, 0)

    def getImagedata(self, name):
        if name == "Grid":
            return self.gridSpriteData
        if name == "Tile":
            return self.tiledata

    def getStringImages(self):

        msgwidths = {"headline" : 312,
                     "redwin"   : 288,
                     "yellowwin": 360,
                     "tie"      : 600,
                     "replay"   : 576}

        msg = {"headline"  : """eJztk8EOwzAIQ/v//7Td9l877ZQpSrAxNHXlUxWBeYbX+/OyLMuyLMuyrMfo+n21jx8oFp9YnW7pXKGv3LaeT+3jB8p3+teP7xTPy3daRZ5ep1s6vlNiXgI+3dJZaVprLMan26rT/Qj4xDzHjHULdzJFyWrl5ZUHqkmUdM7d+MQ8x4x1C3cyRclq5eWVB6pJlHTO3fjEPMeM5ZkHp1BmutVr5U8eHxaNybYo8W75Ye2G8g0YU14vMG46H1Bjr5U/eXxYNEYbdGN0P6zdUL4BY8rrBcZN5wNq7LXyJ48Pi8Zog26M7oe1G8o3YEyCXjFjdD6gxl4rfwTBHc85r47vFDfWf398pxrOeXV8p7ix/vvjO9Vwzqtzxp1ew6c01n9/QCz0KU7lnFdH+YY14GTrwIU8dX9ALPQpTuWcV0f5hjXgZOvAhTx1f0As9ClO5ZxXR/lGOWDMz/gpOdM1NgUPJMZnq/KNONN7sTjT6+SB2moBztVtfyZNa3NfqXwjzvReLM70OnmgtlqAc3Xbn0nT2txXKt+IM70XizO9jgCmZVmWZVmWZd1LX5CqQQk=""",
               "redwin"    : """eJztlEsSwjAMQ7n/ncqOe8EaZtLE+tQNYrTKRI39LHM8X0cURVEURVEU/eix8ru82q+aN3sr6qxaErJffd6KOiv7dfe3os4C94t12bCet94vFp8mf4NLkTD0NbCDfMAx0WHq5u4Mkq54kI+BM4tPLWO1vgZ2kA84JjpM3dydQdIVD/IxcGbxqWWs1tfADvIBxwRednIGQdX4GMqgM3TyWTrR9a5jaHANLtP7Ogu1KWPOMnTZMPBZOtH1rmNocA0u0/s6C7UpY84ydNkw8Fk60fWuYwi6wCCBdgOfmuj10EHV7Do+Myc6zrr8gC5w7qDdwKcmej10UDW7js/MiY6zLj+gC5w7aDfwqYleDx1Uza7jM3Oi46zLj86lq/nat7rxcTLU8dl1v3Sz6JaNXflkv/rc0fG5tuZ/5pP96nNHx4dV/OC39Bb4HQMfZ19NsMxAqNUD9kWfl9PlDADrO6Do8wL7aoJlBkKtHrAv+rycLmcAWN8BRZ8X2FcTLDMQavWAfdHnBbqiKIqiKIqi6KM3YxbLbw==""",
               "yellowwin" : """eJztlDtuxFAMA3P/O2263CvFIpUBA6ZGfFyEBkt9noaEX98/r6qqqqqqqqqqqiR9/X2GriNPDXl8rEqjeutREvrf+Ocqjeqt/jcCHx+r0qje2vsVaH+k6+e8C58zvAvnfJYGzgefY+CjBWCYH5zP0KYQPtRd+BzKd8OlTqoUH3yOgY8WgGF+cD5Dm0L4UHfhcyjfDZc6qVJ88DkGPloAhvnB+eDt2l34C6nY7G3XaobAKRp4DvcYOkX5lVaDkx/GOPYuw3atZgiconHz4bsMLu+9WfMrrQYnP4xx7F2G7VrNEDhF4+bDdxlc3nuz5ldazR754XY8hyF34TVaDinhOaQCgOeH4qP5lVZDdeHbcd9D7sJrtBxSwnNIBQDPD8VH8yuthurCt+O+h9yF12g5pITnkAoAnh+Kj+ZXWg3Vlbb9E+/SarQcOqmezfPeHG2X5ldajZPYXpdz8t5dWo2WQyfVtDyf5aP5lVbjJLbX5Zy8d5dWo+XQSTUtz2f5aH6l1TiJae03HzXZOYc659p1xG48h0Pfh+1OUEPftbtwv4Zd+Jy9AJydQ51z7aKMw12m+DzaRXHeAzX0XbsL92vYhc/ZC8DZOdQ51y7KONxlis+jXRTnPVBD37W7cL+GXVVVVVVVVVVVVSH6BcRUuEE=""",
               "tie"       : """eJztlVGOgzAQQ3v/O3X/eq89ACodZhzbASN/RQmxnwfx/vu8oyiKoiiKoiiKoiiKoiiKnqTX7Dl5oTxaFHHkNvNaP240nqOQb9PIfzCKhnKb+fwHn6mQb9PIfzCKhnKb+fwHn6mQX0Gj9x54F5f+wky8Q2Pw46ji/Dkza7p0lwlnbXFDzwQ+KGNyaN+sMgOijq/LpaUKL9dkDk/8SObneGpYnD9nZk2X7jLhrC1u6JnAB2VMDu2bVWZA1PF1ubRU4eWazOGJH8n8HE8Ni/PnzKzp0l0mnLXFDT0T+KCMyaF9s8oMiDq+LpcbVRM+62gwyR/3VFaewxme69dQb+ZnXXG92+F8mJG1nHvkCeZ7nVZWmAGHtxNqIlQp8cOcn7tyhuf6NdSb+VlXXO92OB9mZC3nHnmC+V6nlRVmwOHthJoIVUr8MOfnrpzhuX4N9WZ+1hXXux3OhxlZy7lHnmC+12llhRlQO8+ourWdrgtYmZatOQ/99BgS9pjkIjQIPyXJhSqOQBU+SAQ+xz2VFUlASV9DvHChcqECVqZla85DPz2GhD0muQgNwk9JcqGKI1CFDxKBz3FPZUUSUNLXEC9cqFyogJVp2Zrz0E+PIWGPSS5Cg/BTklyo4ghU4YNE4HPcU1mRBJT0xZyxXkBU76g9vflx48ycXrc9zFxuDbrlur2fofl130VlhSntPGuzV4ydPBI+vflx44zyo+3CP5dbg265bu9naH7dd1FZYUo7z9rsFWMnj4RPb37cOKP8aLvwz+XWoFuu2/sZml/3XVRWmDKZ51fhkfDR5oLPjxtnOB9tX7bvIfR16TiqL0Kurf2gzK/zM3QIDwg/1eMz5EwQM1dlbLbmDOej7cv2PYS+Lh1H9UXItbUflPl1foYO4QHhp3p8hpwJYuaqjM3WnOF8tH3ZvofQ16XjqL4Iubb2gzK/zs/QoaSd58jkc4iiKIoiifIfjKIoip6s/AejKIrupH+lC6c3""",
               "replay"    : """eJztlduRAyEMBDf/nHx/zusCcBUGNEKNPFv9uZLnAevX3/tljDHGGGOMMcYYY4wxQ56Vp1ytGUCrCaIHIgOL8/lNevTu/6820GqC6IHIwOJ8fpMevfv/qw20miB6IDKwOJ/fpEfvSy72Xt77H1T9jdL21DZ4MihILD16H8gI6lGN7+mZeZkWeElxKj15sdTqyXu5ti/antoGTwYFiaVH7wMZQT2q8T09My/TAi8pTqUnL5ZaPXkv1/ZF21Pb4MmgILH06H0gI6hHNb6nZ+ZlWuAlxan05MVSq0f1ctCF6idUYZaUchIb3J46GV2enjwXtLvc1dfSnufjKfF+souT4q8+Gzdig9tTtHua57RWM+2I0nwt7Xk+nhLvJ7s4Kf7qs3EjNrg9RbuneU5rNdOOKM3X0p7n4ynxLu9i5pGHuadHtbBkD40eLuQGZ6ZORpenJ+jiy11VaC45ojRfQT2qVGl9zcSyl3OeHtXCkj00eriQG5yZOhldnp6giy93VaG55IjSfAX1qFKl9TUTy17OeXpUC0v20OjhQm5wZupkdHl6gi6+3FWF5pIjSvMV1KNKldaX/LeCC2/0flIzNgQsze6p6vNFyyfP4NW+5HpovQ+CynORt5B2VmmasSFgod3ToIvBc3U+eQav9iXXQ+t9EFSei7yFtLNK04wNAQvtngZdDJ6r88kzeLUvuR5a74Og8lwcWKjqfW9cvgdLDxdLTlUHoFlN2AtSGzjEl0qPalzeV56evFLy+gqOy/dg6eFiyanqADSrCXtBagOH+FLpUY3L+8rTk1dKXl/BcfkeLD1cLDlVHYBmNWEvSG3gEF8qPapxeV/yeI0xENpf6ry/CfsyxphC2n/0un7nu/oyxphJ2n/0un7nu/qq5R+7zemZ"""}

        for i in msg.keys():
            msg[i] = base64.b64decode(msg[i])
            msg[i] = zlib.decompress(msg[i])
            msg[i] = pygame.image.fromstring(msg[i], (msgwidths[i], 24), "RGB")
            msg[i] = msg[i].convert(msg[i])

        return msg

    def getAllMatchesInAGridCombinations(self, width, height, matches):

        a = []
        # Horizontals:
        for row in range(height):
            for column in range(width + 1 - matches):
                b = []
                for u in range(matches):
                    b.append(str(u + column) + str(row))
                a.append(b)
        # Verticals:
        for column in range(width):
            for row in range(height + 1 - matches):
                b = []
                for u in range(matches):
                    b.append(str(column) + str(u + row))
                a.append(b)

        # DiagonalsTopLeftToDownRight:
        # -2 to +3:
        for d in range(- (height - matches), (width - matches) + 1, 1):
            for i in range(width - matches):
                b = []
                for u in range(matches):
                    x = i + u
                    y = i + u
                    if d > 0:
                        x += d
                    if d < 0:
                        y -= d
                    if x >= width or y >= height:
                        break
                    b.append(str(x) + str(y))
                if len(b) == matches:
                    a.append(b)

        # DiagonalsTopRightToDownLeft:
        # -3 to +2:
        for d in range(- (width - matches), (height - matches) + 1, 1):
            for i in range(width - matches):
                b = []
                for u in range(matches):
                    x = width - 1 - i - u
                    y = i + u
                    if d < 0:
                        x += d
                    if d > 0:
                        y += d
                    if x < 0 or y >= height:
                        break
                    b.append(str(x) + str(y))
                if len(b) == matches:
                    a.append(b)

        return a

if __name__ == '__main__':
    Main()
I'm quite happy with that. It's also good to see, the forum lets me post a slightly longer piece of code.
Post Reply