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

Spectrum-Environment for Pygame

Post 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?
FFoulkes
Microbot
Posts: 161
Joined: Thu Feb 07, 2019 2:42 pm

Re: Spectrum-Environment for Pygame

Post 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. :)
FFoulkes
Microbot
Posts: 161
Joined: Thu Feb 07, 2019 2:42 pm

Re: Spectrum-Environment for Pygame

Post 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.
dfzx
Manic Miner
Posts: 673
Joined: Mon Nov 13, 2017 6:55 pm
Location: New Forest, UK
Contact:

Re: Spectrum-Environment for Pygame

Post 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.
Derek Fountain, author of the ZX Spectrum C Programmer's Getting Started Guide and various open source games, hardware and other projects, including an IF1 and ZX Microdrive emulator.
FFoulkes
Microbot
Posts: 161
Joined: Thu Feb 07, 2019 2:42 pm

Re: Spectrum-Environment for Pygame

Post 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.
Ralf
Rick Dangerous
Posts: 2279
Joined: Mon Nov 13, 2017 11:59 am
Location: Poland

Re: Spectrum-Environment for Pygame

Post 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/
hikoki
Manic Miner
Posts: 576
Joined: Thu Nov 16, 2017 10:54 am

Re: Spectrum-Environment for Pygame

Post 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
User avatar
PeterJ
Site Admin
Posts: 6855
Joined: Thu Nov 09, 2017 7:19 pm
Location: Surrey, UK

Re: Spectrum-Environment for Pygame

Post by PeterJ »

There is a full Spectrum Emulator written in Python and Pygame here;

https://www.pygame.org/project-PyZX-173-.html
hikoki
Manic Miner
Posts: 576
Joined: Thu Nov 16, 2017 10:54 am

Re: Spectrum-Environment for Pygame

Post 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
User avatar
ZXDunny
Manic Miner
Posts: 498
Joined: Tue Nov 14, 2017 3:45 pm

Re: Spectrum-Environment for Pygame

Post 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.
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