Page 1 of 1

Spectrum-Environment for Pygame

Posted: Wed Feb 13, 2019 9:53 pm
by FFoulkes
Hi there,

a Spectrum-esque environment in Python/Pygame:

Image

Although it's amazing to program the real (or emulated) Spectrum in C, for me it's also rather exhausting.
On the other hand, I know Python quite well, and Pygame would be ok.
I've never seen a really good game in Pygame, though. Maybe it needs some limitations to give it a frame.
dfzx pointed out, that in games programming on the Spectrum, you mostly poke everything directly into the screen buffer. I though, maybe I could do that in Pygame and make it look (and feel) really Spectrum-like.
Well, that's how far I've come (in a few days yet):

- Can set border and paper,
- can plot something (256x176),
- can set coloured paper-spots (32x22),
- can write like "PRINT AT".

I'm not sure, how fast this is (fast enough for "Manic Miner"?). But hey, if a 3.5 Mhz Z80 can beat your libraries on today's Ghz-PC, the libraries wouldn't be worth it anyway.

What do you think?

By the way: Can I get in trouble, when I use the Spectrum's charset (stripped from its ROM), its look-and-feel or the words "Spectrum" and "ZX" in my code?

Re: Spectrum-Environment for Pygame

Posted: Thu Feb 14, 2019 4:54 pm
by FFoulkes
The exercise from here.

Remember: The screenshot doesn't show an emulator-screen, but a Pygame-screen.
Code-snippet is as easy as this:

Code: Select all

        self.zxenv.printAt(10, 6, "CONNECT 4")
        for i in range(11, 18, 1):
            for u in range(10, 16, 1):
                self.zxenv.printAt(i, u, chr(144), "blue", "white")
UDG-definition was much easier than expected: Just added another tuple with 8 numbers to the charset-dictionary with key "chr(144)".

Image

Cool. :)

Re: Spectrum-Environment for Pygame

Posted: Fri Feb 15, 2019 1:05 am
by FFoulkes
I just realized, in my environment, I'm not limited to the Spectrum's memory. So I can have more than just 22 UDGs. In fact, I can have as many UDGs as a Python-dictionary can hold entries. That is, about as many as I want. Thousands.

Re: Spectrum-Environment for Pygame

Posted: Fri Feb 15, 2019 8:06 am
by dfzx
But... but... but... it's not a Spectrum, is it?

It's an environment on a modern machine which looks a bit like a Spectrum, but uses modern technology to circumvent the restrictions of the original, which in turn removes most of the challenge. And the challenge of getting the most out of the 1980s technology is what keeps us drawn to the machine.

Not that I'm knocking you for what you're doing - whatever floats your goat - but a Spectrum with thousands of UDGs isn't a Spectrum for most of us.

Re: Spectrum-Environment for Pygame

Posted: Fri Feb 15, 2019 3:03 pm
by FFoulkes
dfzx wrote: Fri Feb 15, 2019 8:06 amBut... but... but... it's not a Spectrum, is it?
True, it's not a Spectrum ... I'm sorry.

I guess everybody has his own approach to this. You're probably quite happy with the "Spectrum Next" too.
I thought, hmm, 3.5 Mhz, do I want that today, still?
It's more the look and feel that I love. The simplicity of just one set graphics mode. The ability to populate it with text and colour easily, wherever you like. Draw lines with a single command. I always liked that.
(Wasn't that easy on other machines. Think of C64, Amiga or PC. Could you just do a "PAPER 4"? No. That kind of thing.)

And I always wanted to get into Pygame; so this project is a good motivation for me.
You're mileage may vary. That should be ok.
------

By the way: I'm also not limited to the traditional 8x8-matrix. I can draw pixel-art of whatever size, which makes those things sprites, I guess.

Re: Spectrum-Environment for Pygame

Posted: Fri Feb 15, 2019 5:14 pm
by Ralf
As long as you have fun, do it :)

It's not Spectrum but it has been done before - take Zx Spectrum Basic, take some features of it that you think that are cool, add some new features and program it in some modern language on a modern computer.

There was once a project called SpecBas which did a bit similar stuff:
https://sites.google.com/site/pauldunn/

Re: Spectrum-Environment for Pygame

Posted: Sat Feb 16, 2019 7:03 am
by hikoki
Interesting project. It would be cool to code Spectrum games in a subset of Python. Learn Python and see what the interpreted results would be like in a real Spectrum without compiling! Who knows if the subset could be assisted by the ide so the result was compatible with cython code which in turn would be compatible with z88dk C

Re: Spectrum-Environment for Pygame

Posted: Sat Feb 16, 2019 7:22 am
by PeterJ
There is a full Spectrum Emulator written in Python and Pygame here;

https://www.pygame.org/project-PyZX-173-.html

Re: Spectrum-Environment for Pygame

Posted: Sun Feb 17, 2019 12:40 pm
by hikoki
Clivethon would only provide a tiny subset of the huge Python language. Clivethon programs would be able to run in Python 3 so that any knowledge gained in learning Clivethon will transfer directly to learning Python. Besides the teaching scope, there you go the sweet part of the deal, Clivethon programs could be translated into Z80 so coool machine cooode!
Spoiler
You decide if we are indeed Mr Pixel's offspring trying to inspire the world. :)

There you go come and go some links to inspire the world.

Translate Python into assembler
https://benhoyt.com/writings/pyast64/

DSL with Python
https://dbader.org/blog/writing-a-dsl-with-python

Python-subset interpreter
https://github.com/louisyang2015/interpreter

http://techfeast-hiranya.blogspot.com/2 ... on-on.html

Irony for NET
https://www.codeproject.com/Articles/25 ... t-Compiler

Re: Spectrum-Environment for Pygame

Posted: Sun Feb 17, 2019 11:43 pm
by ZXDunny
Ralf wrote: Fri Feb 15, 2019 5:14 pm As long as you have fun, do it :)

It's not Spectrum but it has been done before - take Zx Spectrum Basic, take some features of it that you think that are cool, add some new features and program it in some modern language on a modern computer.

There was once a project called SpecBas which did a bit similar stuff:
https://sites.google.com/site/pauldunn/
It's still going: https://www.youtube.com/user/ZXSpin/videos

And still under development.

Re: Spectrum-Environment for Pygame

Posted: Sat Feb 23, 2019 10:09 pm
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:

Re: Spectrum-Environment for Pygame

Posted: Fri Mar 01, 2019 11:50 pm
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.

Re: Spectrum-Environment for Pygame

Posted: Mon Mar 11, 2019 4:51 pm
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.