Moninpelattava Tetris Pythonilla

empty 17.04.03 16:29

Tekstipohjainen Python-Tetris jossa voi olla 1-3 pelaajaa

 Tekstiversio  Arvo: 4 (6 ääntä)  Äänestä: +  -
# 1-3 pelaajan Tetris Pythonilla. Ominaisuuksina mm.
# pistelaskuri, palkkien nopea tiputtaminen, pyörittäminen
# kumpaankin suuntaan, vihollispelaajien rankaisu kun rivejä
# saa tuhottua, eri väriset palikat sekä seuraavan palikan näyttäminen.
#
# Ajamiseen tarvitset Console-moduulin, jonka saa täältä
# http://effbot.org/zone/console-handbook.htm
#
# Tai tarkemmin ottaen täältä:
# http://effbot.org/downloads/
#
# Tarvitset myös Python version 2.2 tai uudemman (luulisin).
# Testattu Python 2.2.2:lla.
#
# Yksinpeliä pelataan nuolinäppäimillä + ctrl:lla, kaksinpelissä
# on lisäksi w,s,a,d,q ja kolminpelissä i,k,j,l,u. <esc> lopettaa.
#
# Ohjelma havainnollistaa olioiden hyödyllisyyttä: Moninpelin
# lisääminen oli simppeliä kun yhden pelaajan toiminnallisuus oli
# eristetty yhteen luokkaan, eikä missään käytetä globaaleja muuttujia.
#
# Tässä olisi pitänyt käyttää docstringejä dokumentointiin mutta
# tämä on kuitenkin tarkoitettu enemmänkin luettavaksi kuin
# käytettäväksi, joten käytin näitä peruskommentteja.
#
# Hannu Kankaanpää

import Console
import random
import time

# Block on luokka joka kuvaa yhtä Tetris-palikkaa.
# Palkin muoto saadaan parhaiten selville iterate-metodin avulla
# (lue ko. metodin ohjeet jotta saat tarkemmat ohjeet)
class Block:
   
    # Luo uuden palikan.
    # data -- merkkijonojen taulukko, jossa merkki ' ' tarkoittaa
    #         ettei kohdassa ole palkkia.
    #         Esim. ["# ", "##", "# "] on yksi tetrispalikka.
    # origo -- palkin keskipiste tuplena (x, y).
    #          Esim. (0, 0) tarkoittaa palkin vasenta ylänurkkaa
    #          Myös float-luvut ovat sallittuja, esim (1.5, 0.5)
    # color -- palkin väri, 1 - 15
    def __init__(self, data, origo, color):
        # aseta palkin väri
        self.color = color
       
        # blocks on neljän elementin taulukko eri suuntiin
        # kääntyneistä palkeista
        self.blocks = []
       
        # Yhden palkkielementin rakenne on seuraava:
        # block[0] -- palikan "data", eli 2-ulotteinen taulu joka
        #             kertoo missä on palkkia ja missä ei
        # block[1] -- palikan koko muodossa (leveys, korkeus)
        # block[2] -- origon paikka muodossa (x, y)
        self.blocks.append((data, (len(data[0]), len(data)), origo))
       
        # Tee 3 muuta palkkia kääntämällä edellistä palkkia 90 astetta
        for i in range(1, 4):
            # prev on edellinen palkki
            prev = self.blocks[i-1]
           
            # Tee uudelle palkille tyhjä "data"-kenttä
            newblock = [[' ' for i in range(prev[1][1])]
                        for e in range(prev[1][0])]
           
            # Kopioi uuteen palkkiin edellinen palkki käännettynä 90 astetta
            for x in range(prev[1][0]):
                for y in range(prev[1][1]):
                    newblock[x][-1-y] = prev[0][y][x]
           
            # Lisää uusi palkki blocks-taulukkoon.
            # Leveys saadaan edellisen palkin korkeudesta,
            # korkeus edellisen palkin leveydestä.
            # Origon paikka vastaavasti kuin edellä kääntäessä dataa
            self.blocks.append((["".join(i) for i in newblock],
                                (prev[1][1], prev[1][0]),
                                (prev[1][1] - prev[2][1] - 1, prev[2][0])))
   
    # Palauttaa tiettyyn suuntaan käännetyn palkin
    # angle on luku 0-3
    # Palautetun palkkielementin rakenne on seuraava:
    # block[0] -- palikan "data", eli 2-ulotteinen taulu joka
    #             kertoo missä on palkkia ja missä ei
    # block[1] -- palikan koko muodossa (leveys, korkeus)
    # block[2] -- origon paikka muodossa (x, y)
    def getRotated(self, angle):
        return self.blocks[angle]

    # Tämä funktio käy läpi jokaisen palkin palikan ja kutsuu
    # kullekin funktiota f. f on funktio joka ottaa kaksi
    # argumenttia, x:n ja y:n. Nämä vastaavat palkin koordinaatteja
    def iterate(self, rotation, f):
        block = self.getRotated(rotation)
        for x in range(block[1][0]):
            for y in range(block[1][1]):
                rx = x - int(block[2][0])
                ry = y - int(block[2][1])
                if block[0][y][x] != ' ':
                    f(rx, ry)

# Pelilauta yhdelle pelaajalle.
class Board:
    # Tässä on määritelty Tetriksen palikat. Selkeyden vuoksi
    # käytin nimettyjä parametrejä origolle ja color:lle
    blocks = [Block(["# ", "##", "# "],   origo = (0, 1),     color = 11),
              Block(["##", "##"],         origo = (0.5, 0.5), color = 3),
              Block(["#", "#", "#", "#"], origo = (0, 1.5),   color = 6),
              Block(["# ", "# ", "##"],   origo = (0.5, 1),   color = 5),
              Block([" #", " #", "##"],   origo = (0.5, 1),   color = 13),
              Block([" ##", "## "],       origo = (1, 0.5),   color = 2),
              Block(["## ", " ##"],       origo = (1, 0.5),   color = 10)]
   
    # Kuinka paljon saa pisteitä kun tuhoaa eri määrän rivejä?
    pointIncreases = [1, 4, 10, 30, 120]
   
    # Mikä on tyhjän ruudun koodi?
    emptyBlock = 0

    # Luo peliareenan.
    # console on konsoli mihin piirretään
    # x, y -- peliareenan sijainti ruudulla
    # width, height -- peliareenan dimensiot
    # keys -- 5 merkkijonon taulukko joka kertoo pelaajan napit
    def __init__(self, console, x, y, width, height, keys):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.keys = keys
        self.console = console
        # Luo tyhjä peliareena
        self.area = [[Board.emptyBlock for i in range(width)]
                     for e in range(height)]
        # Peli ei ole ohi vielä ;)
        self.gameover = False
        self.points = 0
        # Valitse seuraavaksi palikaksi joku satunnainen palikka
        self.nextBlock = random.choice(Board.blocks)
        # Luo ruudulle uusi palikka
        self.createBlock()
        # self.enemies on lista vihollisten laudoista
        self.enemies = []
   
    # Tällä lisäillään viholliset, niin että tiedetään ketä
    # rankaista kun saadaan rivejä tuhottua.
    def addEnemy(self, enemyBoard):
        self.enemies.append(enemyBoard)

    # Tämä funktio luo uuden palikan, valitsee mikä
    # on seuraava palikka (self.nextBlock) sekä päivittää
    # seuraavan palikan kuvaa
    def createBlock(self):
        self.block = self.nextBlock
        self.block_x = self.width / 2 - 1
        self.block_y = 0
        self.block_rotation = 0
        self.block_movewait = 5
        self.nextBlock = random.choice(Board.blocks)
        self.drawNextBlock()

    # Tätä kutsutaan monta kertaa sekunnissa jotta
    # pelissä tapahtuisi jotain. keypressed on merkkijono
    # joka kuvaa viimeksi painettua nappia
    def move(self, keypressed):
        # Älä liikuttele mitään jos peli on ohi
        if self.gameover:
            return
           
        # Ota palkin vanha sijainti ja rotaatio talteen
        prev_x = self.block_x
        prev_rotation = self.block_rotation
       
        # Tee eri toimintoja eri napeista
        if keypressed in self.keys:
            # self.keys-taulukosta saadaan selville
            # mitä toimintoa painettu nappi vastaa
            keynum = self.keys.index(keypressed)
            if keynum == 0:
                # Liikuta palkkia vasemmalle
                self.block_x -= 1
            elif keynum == 1:
                # Liikuta palkkia oikealle
                self.block_x += 1
            elif keynum == 2:
                # Pyöritä palkkia myötäpäivään
                self.block_rotation = (self.block_rotation + 1) % 4
            elif keynum == 3:
                # Pyöritä palkkia vastapäivään
                self.block_rotation = (self.block_rotation - 1) % 4
            else:
                # Tiputa palkki maahan. Eli luuppaa kunnes
                # osutaan seinään (self.doesHit())
                while not self.doesHit():
                    self.block_y += 1
                # 'Pudota' palkki peliareenalle
                self.dropBlock()

        # Jos siirto aiheutti seinään törmäämisen niin
        # palautetaan vanha sijainti ja rotaatio.
        if self.doesHit():
            self.block_x = prev_x
            self.block_rotation = prev_rotation

        # block_movewait estää ettei palkit tipu ihan
        # koko ajan.
        if self.block_movewait > 0:
            self.block_movewait -= 1
        else:
            self.block_movewait = 5
            # Tiputa palkkia alaspäin
            self.block_y += 1
            # Jos palkki törmää seinään, se pudotetaan peliareenalle
            if self.doesHit():
                self.dropBlock()

    # Tämä funktio huolehtii palkin pudottamisesta
    # peliareenalle. Täällä myös katsotaan tuhoutuuko rivejä,
    # ja lisäillään pelaajan pisteitä sen mukaan
    def dropBlock(self):
        # Aluksi nostetaan palkkia vähän ylöspäin ettei se olisi
        # seinän sisässä.
        self.block_y -= 1
       
        # Tämä pieni apufunktio piirtää palkin peliareenalle.
        def setWall(x, y):
            self.area[y][x] = self.block.color
       
        # self.iterateCurrentBlock käy nykyisen liikuteltavan
        # palikan läpi ja kutsuu jokaisessa kohdassa funktiota
        # joka sille on annettu parametrinä
        self.iterateCurrentBlock(setWall)
       
        # Seuraava koodinpätkä huolehtii rivien tuhoamisesta.
        # Hieman hankala ja epäoleellinen selittää, koeta
        # ymmärtää koodista ;).
        copyTarget = copySource = self.height - 1
        linesErased = 0
        while copyTarget >= 0:
            while copySource >= 0 and \
                    self.area[copySource].count(Board.emptyBlock) == 0:
                copySource -= 1
                linesErased += 1

            if copySource >= 0:
                self.area[copyTarget] = self.area[copySource]
            else:
                self.area[copyTarget] = [Board.emptyBlock
                                         for i in range(self.width)]
            copySource -= 1
            copyTarget -= 1
       
        # Lisää pisteitä sen mukaan kuinka monta riviä tuhoutui
        self.points += Board.pointIncreases[linesErased]
       
        # Rankaise vihollisia jos rivejä tuhoutui
        if linesErased > 0:
            for enemy in self.enemies:
                enemy.punish(linesErased)
       
        # Luodaan vielä lopuksi uusi palikka
        self.createBlock()
       
        # Jos uusi palikka osuu heti seinään, on peli ohi
        if self.doesHit():
            self.gameover = True
       
    # Tarkistaa osuuko tällä hetkellä liikuteltava palikka seinään
    def doesHit(self):
        # hits on apumuuttuja joka kertoo osutaanko seinään vai ei.
        # Lue pep-227 jotta ymmärrät miksi hits on taulukko eikä boolean
        # http://www.python.org/peps/pep-0227.html
        # (etsi sivulta "bank_account")
        hits = [False]
       
        # Tämä apufunktio testaa osutaanko seinään tai
        # pelialueen ulkopuolelle
        def testHit(x, y):
            if x < 0 or x >= self.width or \
               y >= self.height or self.area[y][x] != Board.emptyBlock:
                hits[0] = True

        # Taas käydään nykyinen palikka läpi, tällä kertaa funktion
        # testHit-avustuksella
        self.iterateCurrentBlock(testHit)
        return hits[0]

    # Käy tällä hetkellä liikuteltavan palikan läpi ja kutsuu jokaiselle
    # palikan palkille funktiota f.
    # f ottaa kaksi argumenttia, x:n ja y:n, jotka vastaavat sijaintia
    # pelikentällä.
    def iterateCurrentBlock(self, f):
        # Taas apufunktio. Tämä delegoi block.iterate:n antaman
        # x:n ja y:n funktiolle f. Mutta ensin lisää ko. x:ään ja y:hyn
        # palikan sijainnin peliareenalla.
        def func(x, y):
            if y + self.block_y >= 0:
                f(x + self.block_x, y + self.block_y)
        # self.iterateBlock on vastaava funktio mutta toimii
        # mille tahansa palikalle.
        self.block.iterate(self.block_rotation, func)

    # Piirrä teksti 'Next:' sekä sen alle seuraavaksi tuleva
    # palikka
    def drawNextBlock(self):
        # Piirrä 'Next:'-teksti
        self.console.text(self.x + self.width + 3, self.y + 3, "Next:")
       
        # Pyyhi edellinen palikka pois, käyttäen vihreitä pisteitä
        self.console.rectangle((self.x + self.width + 3, self.y + 4,
                                self.x + self.width + 9, self.y + 10),
                               2, '.')
       
        # Laske väri seuraavaksi tulevalle palikalle
        color = self.nextBlock.color * 16
       
        # Tämä funktio piirtää palkkia ruudulle
        def func(x, y):
            self.console.text(x + self.width + self.x + 5,
                              y + self.y + 6, ' ', color)
       
        # Ja nyt käydään seuraava palikka läpi (kulma 0), piirtäen
        # jokainen sen palkki ruudulle func:n avulla.
        self.nextBlock.iterate(0, func)

    # Rankaistaan itseämme :). Tätä kutsuvat vihollispelaajat kun
    # saavat rivejä tuhotuksi. amount on 1 - 4, rankaisun määrä
    def punish(self, amount):
        for i in range(amount):
            # Näin saadaan yksi rivi tuhottua yhdellä rivillä
            self.area = self.area[1:]

            # Lisätään vielä pohjalle rivi jolla on satunnaista roskaa
            # värillä 7.
            garbage = [random.randint(0, 1) * 7 for i in range(self.width)]

            # Varmistetaan ettei tule riviä joka on täysi palkki
            if garbage.count(Board.emptyBlock) == 0:
                garbage[random.randrange(0, self.width)] = Board.emptyBlock

            self.area.append(garbage)
            if self.doesHit():
                self.dropBlock()

    # Tämä funktio piirtää peliareenan, pisteet sekä liikuteltavan
    # palkin ruudulle
    def draw(self):
        # Ensin piirretään peliareena
        for xx in range(self.width + 2):
            for yy in range(self.height + 1):
                # Tämä piirtää reunat
                if xx == 0 or xx == self.width + 1 or yy == self.height:
                    self.console.text(xx + self.x, yy + self.y, '#', 0x87)
                # Tämä piirtää tyhjää
                elif self.area[yy][xx - 1] == Board.emptyBlock:
                    self.console.text(xx + self.x, yy + self.y, '.', 1)
                # Tämä piirtää palkin jos peli on loppunut (harmaa '#')
                elif self.gameover:
                    self.console.text(xx + self.x, yy + self.y, '#')
                # Tämä piirtää normaalin värikkään palkin
                else:
                    self.console.text(xx + self.x, yy + self.y,
                                 ' ', self.area[yy][xx - 1] * 16)

        # Jos peli ei ole päättynyt, piirretään liikuteltava palkki
        if not self.gameover:
            # Palkin väri
            color = self.block.color * 16
            # Ja tässä näppärästi yhdellä lauseella palkin piirtäminen.
            # lambda luo nimettömän funktion joka on passeli tähän
            # paikkaan.
            self.iterateCurrentBlock(lambda xx, yy:
                self.console.text(xx + self.x + 1, yy + self.y, ' ', color))

        # Piirrä pisteet
        self.console.text(self.x + self.width + 3, self.y, "Points:")
        self.console.text(self.x + self.width + 2, self.y + 1,
                          str(self.points).rjust(7))
       

#################################################################
#
# Täältä itse peli alkaa
#
#################################################################
def tetris():
    # Luodaan eka konsoli
    console = Console.getconsole()
    console.title("Tetris")

    # Tällä funktiolla voidaan lukea näppikseltä merkki. Palauttaa
    # kyseisen merkin, esim 'w' tai 'Escape' tai 'Left'
    def readKeyboard():
        while console.peek() is not None:
            # event on tapahtuma (esim. hiiren liikahdus ruudulla)
            event = console.get()
            # Mutta me välitetään vain napin painalluksista ('KeyPress')
            if event.type == 'KeyPress':
                if event.keysym:
                    return event.keysym
                else:
                    return event.char

    # Udellaan pelaajien määrä
    print 'How many players? (1,2 or 3)'
    key = None
    while key != '1' and key != '2' and key != '3':
        key = readKeyboard()
    players = int(key)

    # Pyyhitään ruutu tyhjäksi, eli piirrettään kuvaruudun kokoinen
    # suorakaide välilyöntiä (rectangle piirtää vakiona välilyöntiä)
    console.rectangle((0, 0, console.size()[0], console.size()[1]))
   
    # Luodaan jokaiselle pelaajalle lauta, taulukkoon 'boards'.
    boards = [Board(console, 5, 1, 10, 20,
                    ['Left', 'Right', 'Up', 'Control_L', 'Down'])]
   
    if players >= 2:
        boards.append(Board(console, 30, 1, 10, 20, ['a', 'd', 'w', 'q', 's']))
        # Lisää vihollinen (lauta nro 0)
        boards[1].addEnemy(boards[0])
        boards[0].addEnemy(boards[1])
   
    if players >= 3:
        boards.append(Board(console, 55, 1, 10, 20, ['j', 'l', 'i', 'u', 'k']))
        # Lisää viholliset
        boards[2].addEnemy(boards[0])
        boards[2].addEnemy(boards[1])
        boards[0].addEnemy(boards[2])
        boards[1].addEnemy(boards[2])
   
    while 1:
        # Pikkasen paussia ettei pyöri liian nopeasti
        time.sleep(0.05)
       
        key = readKeyboard()

        # Lopeta luuppi jos painettiin esc:iä
        if key == 'Escape':
            break

        # Käy läpi kaikki laudat, liikuta palikoita niissä
        # ja piirrä ne.
        for i in range(len(boards)):
            boards[i].move(key)
            boards[i].draw()
       
        # Testaa onko yksinpeli vai moninpeli
        if players == 1:
            # Yksinpeli loppuu kun pelaaja kuolee (gameover)
            if boards[0].gameover:
                break
        else:
            # Moninpeli loppuu kun yksi pelaaja on enää elossa
            # (varmuuden vuoksi myös kun 0 pelaajaa on elossa,
            # jos kaksi sattuu kuolemaan juuri yhtäaikaa)
            # Tämän voisi muuten ilmaista ytimekkäämmin näin:
            # alivePlayers = len(filter(lambda x: not x.gameover, boards))
            # Mutta ehkä uuden listan muodostaminen olisi vähän
            # hardcorea tätä tarkoitusta varten
            alivePlayers = 0
            for board in boards:
                if not board.gameover:
                    alivePlayers += 1
            if alivePlayers <= 1:
                break
   
    # Peli loppui, tulosta jotain ruudulle
    console.text(9, 14, "+" + "-" * 24 + "+")
    console.text(9, 15, "|" + "Game over!".center(24) + '|')
    console.text(9, 16, "|" + "Press 'esc' to quit".center(24) + '|')
    console.text(9, 17, "+" + "-" * 24 + "+")
   
    # Ja odota kunnes käyttäjä painaa esc
    while readKeyboard() != 'Escape':
        pass

# Peli käynnistyy vain jos Python on käynnistetty tästä
# moduulista.
if __name__ == '__main__':
    tetris()

pikkumyy 12:40 19.4.03 
Übersvalt!
theril 17:59 21.4.03 
"The Console module is currently only available for Windows 95, 98, NT, and 2000."
empty 16:22 22.4.03 
Sori Theril :P. Toinen vaihtoehto tekstigrafiikkaan olisi ollut Pythonin mukana tuleva curses-moduuli, mutta se taas toimii tällä hetkellä vain Unix:ssa. Seuraavan koodinpätkäni kyllä kirjoitan käyttämään pygame-moduulia, joka löytyy Win/Mac/Unix:lle.
http://www.pygame.org/
empty 11:09 26.4.03 
Muutin Board.doesHit()-funktiota hieman. Nyt se on oikeaoppisempi joskaan ei juurikaan selkeämpi (aiemmin se käytti self.hits-muuttujaa, mutta apumuuttujan tunkeminen olioon oli vähän arvelluttavaa)
empty 08:42 11.5.03 
Kansa tahtoo lisää Pythonia!
ane 21:51 6.8.03 
NIIN! Python-alue tänne!
empty 13:51 13.3.04 
Loistavaa. pyGame viela kayttoon niin tulee mukavempana.

Python sektiota tarvittaisiin.