Päätin hylätä idean tehtä ääntä lähettävän viesti-ohjelman, ja tehdä pelin - ristinollan. Olen lukenut Tkinterin opasta, mutta jotkin asiat eivät käynneet ihan selviksi vielä. Kielenä edelleen Python.
1. Miten tehtiin kaksi eri osaa ohjelmaan, että toisessa voidaan pyörittää toista silmukkaa ja toisessa kysyä vaikka jotain? Näkyvätkö nämä silti yhtenä ikkunana?
2. Voiko Pythonilla piirtää/kirjoittaa tuohon ikkunaan helposti, että voisi toteuttaa ruudukon, X ja O?
3. Pelissä täytyy lukea hiiren sijainti paino hetkellä, jotta tiedetään mitä ruutua painetaan. Jos on 20x20px ruutuja, niin miten lasketaan minkä ruudun sisällä tämä on?
*---*---*---* | | | | *---*---*---* | | | | *---*---*---* | | | | *---*---*---*
Jos ruudukko on tälläinen, niin millaisella laskulla tiedetään minkä ruudun päällä kursori on?
4. Voiton tarkistus. Miten näiden ruksien ja nollien sijainti kannattaa pitää tiedossa? Jos kirjattaisiin ne muuttujaan, niin tulisi tuossa 9 muuttujaa! Taulukossa päästäisiin vähemmällä, onko se järkevin tapa?
1. Luultavasti ajattelet asiaa liian hankalasti. Riittää aivan takuulla, että suoritat ohjelmaa rivi kerrallaan yhdessä säikeessä.
2. pygamen avulla pitäisi onnistua haluttu ominaisuus.
3. Mikäli ruudut ovat olioita, niin luokkaan voidaan kirjoittaa vertailufunktio.
#include <iostream> #include <vector> class Ruutu{ private: int x; int y; int leveys; int korkeus; public: Ruutu(int a, int b); bool onko_alueella(int x, int y); }; Ruutu::Ruutu(int a, int b){ this->x = a; this->y = b; this->leveys = 30; this->korkeus = 30; } bool Ruutu::onko_alueella(int x, int y){ return (this->x < x && x < this->x + leveys && y > this->y && y < this->y + this->korkeus); } int main(){ std::vector <Ruutu> Ruudut; Ruudut.push_back(Ruutu(30, 30)); int tmp_x = 32; int tmp_y = 32; std::cout << Ruudut[0].onko_alueella(tmp_x, tmp_y) << std::endl; std::cout << Ruudut[0].onko_alueella(29, tmp_y) << std::endl; return EXIT_SUCCESS; }
Sitten vain käyt jokaisella kierroksella ruudukon läpi ja katsot mitä tuo onko_alueella() metodi palauttaa, jos hiiren nappi on pohjassa.
En selittänyt tarpeeksi hyvin tuota ykköskohtaa. Tarvitsen siksi kolme säiettä, että yhteen tulee itse peli, toiseen tulee "ikuinen" silmukka joka hakee dataa ja kolmanteen tulee raw_input-kenttä. (Taisin puhua kahdesta tuolla aikaisemmin)
Kakkoskohta. Onko sillä helppo tulostaa asioita, että ei tarvitse kymmentä riviä koodia tulostaakseen tekstiä?
Kolmoskohta. Tämä on ehkä väärä kieli, kenties =) No, mutta kenties tuosta saa selvää.
Luulen, että pärjäät kyllä yhdelläkin säikeellä. Tai jos välttämättä haluat pelata säikeillä, niin pidä huolta, että jokainen säie käisttelee dataa täysin itsenäisesti.
Pygamen sivuilta selviää aika hyvin tuon käyttö.
Mihin tarvitset raw_input-syötettä, jos kuitenkin käytät jotain parempaa kirjastoa? Myöskään datan hakemiseen ei tarvita erillistä säiettä, vaan voit vain tarkistaa toistuvasti, onko dataa tullut. Pääsilmukan rakenne on suunnilleen tällainen:
while True: if on_dataa: lue_data() if on_syotetta: lue_syote() if tilanne_muuttui: piirra_tilanne() nuku(hetki)
Suosittelen äskettäin julkaistun matopelioppaan lukemista. Vaikka kieli on eri, kaikki periaatteet pätevät aivan hyvin.
3. Voit laskea ruudun kaavalla, jonka osana on jakolasku ja pyöristys alaspäin. Mieti asiaa paperilla: miten saat selville ruudun, kun tiedät hiiren sijainnin?
4. Taulukon (tai Pythonissa listan) käyttäminen on hyvä ajatus.
Yksinkertaisimmillaan voit toteuttaa ristinollan peliruudukon Tk:n buttoneilla ja vaihtaa buttonin bitmappia tarvittaessa (esim. X, O tai tyhjä).
Toinen vaihtoehto on käyttää canvas widgettiä ja piirtää ruudukko ja pelimerkit itse canvaksen hyväksymillä komennoilla. Hiiren syötteen saat bindaamalla hiiren napit ja haluttu toiminto canvakseen.
jalski kirjoitti:
Yksinkertaisimmillaan voit toteuttaa ristinollan peliruudukon Tk:n buttoneilla ja vaihtaa buttonin bitmappia tarvittaessa (esim. X, O tai tyhjä).
Hei, hyvä ajatus! Näin sen voisi toteuttaa aluksi, niin se onnistuisi hyvin. =) Sitten siinä ei tarvitsisi laskea hiiren sijaintia. Mutta, buttonilla on sellaisen ärsyttävät reunat, kuten HTML:ssä buttonilla. Miten saan tämän vain isoksi (esim. 20x20px) alueeksi jota painettaessa tapahtuu jotain?
Kokeilepa pistää borderwidth 0 ja relief flat parametreiksi buttonille.
Kiitos, riitti kyllä pelkästään borderwidth =) Katsotaan, kun saan äkkiä demon valmiiksi.
Muokkaus. En saa millään nappeja niin, että olisi 3x3 "ruudukko" niistä. Ne menevät joko kaikki purkeen (vasen - oikea) tai pystysuoraan.
Entä, miten voin laittaa nappulan disaploiduksi?
Nappulan disablointi, aseta: -state disabled.
3x3 ruudukko: Laita vaikka kolmeen frameen kolmenappia vierekkäin kuhunkin ja pakkaa framet allekkain.
jalski kirjoitti:
3x3 ruudukko: Laita vaikka kolmeen frameen kolmenappia vierekkäin kuhunkin ja pakkaa framet allekkain.
Anteeksi?
Miten saisin Tk Label-tulostukset pyyhittyä pois jossain tietyssä vaiheessa? Kuten C++:ssa system("CLS").
Macro kirjoitti:
Miten saisin Tk Label-tulostukset pyyhittyä pois jossain tietyssä vaiheessa? Kuten C++:ssa system("CLS").
Pythonin Tk - toteutuksesta en ole varma, mutta Infernolle Limbolla ohjelmoidessa toimii labelille komento: configure -text { }
Tässä on toimiva esimerkkiohjelma 3x3-ruudukon luonnista Python 3:lla ja Tk:lla:
from tkinter import * pohja = Tk() pohja.title("3x3-ruudukko") kehys = Frame(pohja) kehys.pack() kehys1 = Frame(kehys) kehys1.pack() kehys2 = Frame(kehys1) kehys2.pack() nappi1 = Button(kehys2, text="1",borderwidth=0,command="") nappi1.pack(side=LEFT) nappi2 = Button(kehys2, text="2",borderwidth=0,command="") nappi2.pack(side=LEFT) nappi3 = Button(kehys2, text="3",borderwidth=0,command="") nappi3.pack(side=LEFT) nappi4 = Button(kehys1, text="4",borderwidth=0,command="") nappi4.pack(side=LEFT) nappi5 = Button(kehys1, text="5",borderwidth=0,command="") nappi5.pack(side=LEFT) nappi6 = Button(kehys1, text="6",borderwidth=0,command="") nappi6.pack(side=LEFT) nappi7 = Button(kehys, text="7",borderwidth=0,command="") nappi7.pack(side=LEFT) nappi8 = Button(kehys, text="8",borderwidth=0,command="") nappi8.pack(side=LEFT) nappi9 = Button(kehys, text="9",borderwidth=0,command="") nappi9.pack(side=LEFT) pohja.mainloop()
Jep, kiitos teille. Nyt sain sen ihan oikein. Mutta yksi kysymys pyörii päässä.
Jos nappuloidan tekstit ovat listassa, ja jokaisella nappulalla text arvo on tekstit[monesko_nappula], niin mten saan tarkistettua onko tämä muuttunut viime kierroksesta? Jos laitan silmukkaan nuo nappuloiden tulostukset, niin siitähän tulee miljoona eri nappulaa.
Tallennat edellisellä jokaisella kierroksella muistiin nappulan arvon ja seuraavalla kierroksella tarkistat, onko tämä arvo muuttunut.
Tuo on itsestään selvyys, mutta jos teen yhdellä kierroksella napin, ja seuraavalla kierroksella teen myös, niin niitähän tulee joka kierroksella yksi lisää jos jokin on muuttunut.
kierros += 1 UnboundLocalError: local variable 'kierros' referenced before assignment
Saan tälläisen virheen. Minulla on ohjelman alussa alustettu muuttuja kierros nollaksi, ja funktiossa tarkoitus olisi lisätä siihen yksi.
Nähtävästi tämä ei toimi, kun kierrosta ei ole määritelty funktion sisällä.
Muokkaus. Minulla näyttää koodi tällä hetkellä tältä:
# -*- coding: ISO-8859-1 -*- from Tkinter import * root = Tk() def valikko(item): print "Painettiin", item def helpp(): Label(root, text="Ristinollapelin ohjeet...").pack() kierros = 0 tekstit = ["Tyhjä", "Tyhjä", "Tyhjä", "Tyhjä", "Tyhjä", "Tyhjä", "Tyhjä", "Tyhjä", "Tyhja"] kierros = 1 def ristinolla(nappi): try: if kierros % 2 == 0: # Nollan vuoro tekstit[nappi] = "O" else: # Rastin vuoro tekstit[nappi] = "X" except: print "Virhe!" print nappi menubar = Menu(root) root.title("Ristinolla") root.minsize(width=500, height=350) # Päävalikko filemenu = Menu(menubar) filemenu.add_command(label="Uusi peli", command=valikko("Uusi peli")) filemenu.add_command(label="Tallenna", command=valikko("Tallenna")) filemenu.add_separator() filemenu.add_command(label="Poistu", command=root.destroy) menubar.add_cascade(label="MPS Ristinolla", menu=filemenu) # Help-valikko helpmenu = Menu(menubar) helpmenu.add_command(label="Tietoa", command=helpp) menubar.add_cascade(label="Apua", menu=helpmenu) # Näytetään root.config(menu=menubar) # Luodaan rivit kehys = Frame(root) kehys.pack() kehys1 = Frame(kehys) kehys1.pack() kehys2 = Frame(kehys1) kehys2.pack() nappi1 = Button(kehys2, text=tekstit[0], width=10, height=4, borderwidth=1, command=ristinolla(0)) nappi1.pack(side=LEFT) nappi2 = Button(kehys2, text=tekstit[1], borderwidth=1, width=10, height=4, command=ristinolla(1)) nappi2.pack(side=LEFT) nappi3 = Button(kehys2, text=tekstit[2], borderwidth=1, width=10, height=4, command=ristinolla(2)) nappi3.pack(side=LEFT) nappi4 = Button(kehys1, text=tekstit[3], borderwidth=1, width=10, height=4, command=ristinolla(3)) nappi4.pack(side=LEFT) nappi5 = Button(kehys1, text=tekstit[4], borderwidth=1, width=10, height=4, command=ristinolla(4)) nappi5.pack(side=LEFT) nappi6 = Button(kehys1, text=tekstit[5], borderwidth=1, width=10, height=4, command=ristinolla(5)) nappi6.pack(side=LEFT) nappi7 = Button(kehys, text=tekstit[6], borderwidth=1, width=10, height=4, command=ristinolla(6)) nappi7.pack(side=LEFT) nappi8 = Button(kehys, text=tekstit[7], borderwidth=1, width=10, height=4, command=ristinolla(7)) nappi8.pack(side=LEFT) nappi9 = Button(kehys, text=tekstit[8], borderwidth=1, width=10, height=4, command=ristinolla(8)) nappi9.pack(side=LEFT) mainloop()
Aikas sekava, mutta koitan saada toimimaan. Ensiksi: Nyt kun funktioihin pitää antaa paramenteiksi jotain, niin nämä funktiot suoritetaan heti ohjelman alkuun. Nyt kun painaa valikosta jotakin, niin mitään ei tapahdu. Miten korjaisin niin, että kun painaa valikkosta jotain/nappia niin se suorittaisi funktion?
Macro kirjoitti:
niin niitähän tulee joka kierroksella yksi lisää jos jokin on muuttunut.
Eihän tule, koska sinun pitää luoda kaikki tarvittavat napit tai ruudut ennen pääsilmukkaan menemistä. Virheen pitäisi korjaantua, jos laitat muuttujan 'kierros' globaaliksi.
Käsitin vähän eri tavalla tämän logiikan, tietenkin ne määritellään siellä. =)
Ei auttanut vaikka asetin kerros-muuttujan globaaliksi.
Selkeyttäisin vähän käyttöliittymän määrittelyä. Tämä kannattaa miettiä huolella loogiseksi ja helpoksi hahmottaa.
Nyt ensimmäisen kehyksen parent on root (toplevel ikkuna), toisen on kehys1 ja kolmannen on kehys2. Jos meinaat myöhemmin lisätä tavaraa käyttöliittymään, niin loogisempi olisi märitellä:
- napit frame, jolle parent root
- napitrivi1 frame, jolle parent napit
- napitrivi2 frame, jolle parent on napit
- napitrivi3 frame, jolle parent on napit
- Määrittele ja pakkaa kolme buttonia jokaiseen napitrivi kehykseen.
- pakkaa napitrivi kehykset napit kehykseen.
For-silmukkalla varmaan saisi toteutettua helpommin nämä nappulat. PHP:llä osaisinkin, mutta miten saan tulostettua Pythonilla niitä nappuloita? Osaan kyllä for silmukat sun muut, mutta kun niitä frameja pitää luoda, niin menee vaikeammaksi.
Esimerkki: Käyttäjä antaa luvun 7. For silmukassa tulostetaan 7x7 määrä nappeja.
n = 7 # Luodaan listat. kehykset = [None] * n napit = [None] * n # Luodaan kehykset ja napit. Lisää loput parametrit ja pack-kutsut! for i in range(0, 7): kehykset[i] = Frame(root) napit[i] = [None] * n for j in range(0, 7): napit[i][j] = Button(kehykset[i])
Tuo command-juttu ei koodissasi toimi aivan oikein. Funktiokutsu suoritetaan napin luomisen yhteydessä ja command-parametriksi tulee funktion paluuarvo (tässä None, koska funktio ei palauta mitään). Voit lukea ongelman ja ratkaisun täältä (löytyi listan kärjestä hakusanalla Tkinter button command).
En saanut koodiasi toimimaan halutulla tavalla. Lisäsin parametrit ja packit, mutta ei silti lähtenyt toimimaan.
napit[i][j] = Button(kehykset[i], command=nappula).pack(side=LEFT)
Tulostuksen koitin hoitaa print napit[numero][numero], mutta ei toiminut. (Lisäsin myös kaikki import-kohdat sun muut) Pitikö se noin tehdä?
Kiitos tuosta linkistä, se oli hyvä neuvo =)
Et voi laittaa sitä packia tuohon, vaan se pitää laittaa seuraavalle riville. Nythän sijoitat sen paluuarvon napit-listaan, ja paluuarvo on sattumoisin None eikä mikään nappi. (Aiemmassa koodissasi olet tehnyt sen ihan oikein, joten miksi rikoit sen nyt?)
Ehken ole vielä välttämättä valmis tähän Tk:n harjoitteluun, kun ei näytä millään onnistuvan. Tai, pitäisikö lukea enemmän tai lainata jokin kirja kirjastosta?
Koitin laittaa sen eritavalla, mutta siltikään ei tulostu mitään.
En ymmärrä, että miten laitan sen toisen rivin tuohon, koska buttonilla ei ole nimeä. Miten se pitää tehdä? Nyt on kyllä ideat vähissä.
Koodista saa aivan toimivan, kun lisää framelle ja buttonille pack-rivit heti muodostimien jälkeen.
Se tässä onkin ongelmana...
Sovelsin koodin tässä keskustelussa esitellyistä pätkistä:
from Tkinter import * root = Tk() n = 7 # Luodaan listat. kehykset = [None] * n napit = [None] * n # Luodaan kehykset ja napit. for i in range(0, 7): kehykset[i] = Frame(root) kehykset[i].pack() napit[i] = [None] * n for j in range(0, 7): napit[i][j] = Button(kehykset[i], text = "%d,%d" % (i, j)) napit[i][j].pack(side = LEFT)
Ehkä se helpompien asioiden tekeminen alkuun on ihan viisas ajatus... :)
Yksinkertaisia asioita on hankala keksiä =)
Sellainen juttu vielä, että jos a:n arvo on ennen silmukoita 0, ja joka kierroksella pitäisi lisätä siihen yksi (a += 1), niin miten se onnistuu? Kun laitan commandiksi funktio(a), niin a on aina n * n... (Helppoja asioita on hankala keksiä)
def f(i, j): print i, j def g(i, j): return lambda: f(i, j) a = g(1, 2) b = g(3, 4) a() # lambda: f(1, 2) => f(1, 2) => print 1, 2 b() # lambda: f(3, 4) => f(3, 4) => print 3, 4
Mihin muuten oikeastaan tarvitset silmukoita event-pohjaisessa pelissä, kuten ristinolla?
Jos haluat, että joka toisella painalluksella "ruutuun" tulee "X" ja "O", niin mikset ratkaise tätä vain muutamalla apumuuttujalla:
#En osaa pythonia, mutta jotenkin näin... current = "X" last = "O" # tilan vaihto temp = current current = last last = temp
Missä näet silmukoita? Minusta tässä on keskusteltu nyt vain nappien luomisesta, ja viimeisimmässä koodissa on käytetty kierroksen jakojäännöstä siirtäjän selvittämiseen – aivan oikein ja järkevästi.
jalski kirjoitti:
# tilan vaihto temp = current current = last last = temp
Pythonissa tyylikkäämmin näin:
current, last = last, current
Jäinköhän jostain paitsi? En nyt ihan käsittänyt, että mitä tarkoitat tällä. Nyt tällä tyylillä saan kaikkiin arvon None/(14, 14).
Siis, joka nappulan kohdalla lisättäisiin a-muuttujaan 1. Yhdessä for-silmukassa se olisi helppo, kutta kun niitä on kaksi niin tulee vähän mitä tulee.
Macro kirjoitti:
Siis, joka nappulan kohdalla lisättäisiin a-muuttujaan 1. Yhdessä for-silmukassa se olisi helppo, kutta kun niitä on kaksi niin tulee vähän mitä tulee.
Miten niin? Lisäät vain siellä sisimmässä silmukassa samalla, kun luodaan uusi nappulakin.
Käsitin, että ongelmasi liittyi tuohon command-asiaan. Jos käytät suoraan lambdaa uuden funktion luomiseen, siinä oleva a-muuttuja viittaa globaaliin a:han, joka jatkaa kasvuaan. Esimerkissäni luodaan funktio toisessa funktiossa, jotta siihen saadaan oikeat (muuttumattomat) arvot.
Se kyllä palauttaa aina yhden muuttumattoman arvon, mutta se ei kyllä ole oikea. Jokaisella nappulalla on nyt arvo (14, 14).
# -*- coding: iso-8859-1 -*- from Tkinter import * import tkSimpleDialog import tkMessageBox root = Tk() X = 0 O = 0 #Aluksi koko on 3 * 3 koko = 3 root.title("Ristinolla") root.iconbitmap('favicon.ico') def tallenna(): print "Tallennus" #... def uusi_peli(): if tkMessageBox.askyesno("Uusi peli?", "Haluatko varmasti aloittaa uuden pelin?"): uusi_peli = tkSimpleDialog.askstring("Uusi peli", "Anna nimi uudelle pelille:") if len(uusi_peli) <= 0: uusi_peli = "Uusi peli" X = 0 O = 0 tekstit = ["Tyhjä", "Tyhjä", "Tyhjä", "Tyhjä", "Tyhjä", "Tyhjä", "Tyhjä", "Tyhjä", "Tyhjä"] root.title(uusi_peli) peliruutu = tkSimpleDialog.askstring("Koko", "Peliruudun koko:") koko = peliruutu #Debuggaus print "peliruutu", peliruutu print "koko", koko #Debuggaus print "koko:", koko def ristinolla(palikka): print "Painettiin nappulaa", palikka menubar = Menu(root) filemenu = Menu(menubar) filemenu.add_command(label="Uusi peli", command=uusi_peli) filemenu.add_command(label="Tallenna", command=tallenna) filemenu.add_separator() filemenu.add_command(label="Poistu", command=root.destroy) menubar.add_cascade(label="Ristinolla", menu=filemenu) # Näytetään root.config(menu=menubar) #Debuggaus print "koko: ", koko # Luodaan listat: n kehystä, n*n nappia. kehykset = [None] * koko napit = [[None] * koko] * koko # Luodaan kehykset ja napit. for i in range(0, koko): kehykset[i] = Frame(root) kehykset[i].pack() for j in range(0, koko): napit[i][j] = Button(kehykset[i], text="Tyhjä", command=lambda: ristinolla(a)) napit[i][j].pack(side=LEFT) mainloop()
En oikein ymmärrä tätä tilannetta. Jos avataan uusi peli, niin siinä voidaan myös määrittää laudan koko. Aluksi funktiossa nämä tulostuvat ihan oikein (Sama mitä syötetty), mutta tuon for silmukan edessä on ongelma. koko-muuttuja on taas 3, ja tulee pieni taulukko.
#Debuggaus print "koko:", koko
Kun on aloitettu uusi peli valikosta, niin ohjelma jää tähän, kunnes painetaan nappia. Miksei se jatka suoritusta koodin loppuun asti? Miten saan ohjelman toimimaan niin, että se tietää oikean pelilaudan koon käyttäjän syötteestä?
Muokkaus. Ehditkin muuttaa lähettämääsi nappientulostus-silmukkaa. Sainkin sen tuolla toimimaan, mutta nyt on tämä koko asia enään.
Ja katsopa sitten uudestaan sitä esimerkkiäni. Juuri tuon takia siinä on erillinen funktio, joka palauttaa lambdafunktion. Sinun versiossasi a viittaa globaaliin a-muuttujaan, minun esimerkissäni taas saadaan lambdafunktion i:ksi ja j:ksi g:lle annettujen parametrien arvot kutsuhetkellä.
Ja nyt siis puhun tuosta lambdafunktioesimerkistä enkä mistään nappuloista.
Taisitkin muokata sitä esimerkkiä sen jälkeen, kun kopioin sen. Nyt ne arvot toimivat, mutta on toinenkin ongelma (Edellinen viesti).
Muokkaus. Ei nyt vitsi... Tuo lambda on kyllä kummallinen tapaus. Voitko korjata tuon esimerkkini for-silmukat niin, että vaikka siellä olisi se lambda niin nappien toiminto olisi oikein. Siis niin kuin selitit noilla parilla funktiolla, mitä en ymmärtänyt.
Tarkoitus olisi, että napilla 0 on tehtävä lambda: funktio(0), napilla 1 tehtävä lambda: funktio(1) jne...
Metabolix kirjoitti:
Missä näet silmukoita? Minusta tässä on keskusteltu nyt vain nappien luomisesta, ja viimeisimmässä koodissa on käytetty kierroksen jakojäännöstä siirtäjän selvittämiseen – aivan oikein ja järkevästi.
Pythonissa tyylikkäämmin näin:
current, last = last, current
Viittasin ylempään koodiin, missä on osasena:
def ristinolla(nappi): try: if kierros % 2 == 0: # Nollan vuoro tekstit[nappi] = "O" else: # Rastin vuoro tekstit[nappi] = "X" except: print "Virhe!" print nappi
Ai Pythonissa on tuple -tuki, Limbolla vastaava olisi:
(current, last) = (last, current);
Juu... Nytten olisi vielä se nappulan numerointiongelma (1, 2, 3, 4 jne...) ja kierros-muuttuja. Ohjelman alussa muuttuja kierros asetetaan nollaksi, ja siihen pitäisi lisätä aina 1 funktiossa ristinolla(). Saan kuitenkin virheen
UnboundLocalError: local variable 'kierros' referenced before assignment
sun pitää välittää parametrina se kierros tolle funktiolle. Ja jos tarviit vielä kutsujassa sen keirros muuttujan arvoa, sun pitää vielä laittaa funktio palauttamaan se.
Tein tälläisen systeemin testiksi:
def funktio(nappula, kierrokset): print "Nappula", nappula kierros = kierrokset + 1 print "Kierros:", kierros, "\n" while True: nappi = raw_input("Nappula: ") funktio(nappi, kierros)
Tämän mukaan kierros on joka kierroksella 1. Kun funktiossa lisätään kierros-muuttujaan joka kerralla kierros-muuttujan arvo + 1, niin miksei se kasva joka kerta, vaan on aina aluksi 0? Mitä pitää tehdä?
Kyseessä on muuttujien näkyvyysalueet. Kierros
-muuttuja on näkyvillä vain funktion funktio
sisällä, eikä sen muutokset välity ulkopuolelle. Tästä päästäänkin erääseen Pythonin erikoisuuteen; pääperiaate on, että kaikki muuttujat välitetään funktioon viittauksena, paitsi numero- ja tekstityypit:
http://www.penzilla.net/tutorials/python/
Python passes all arguments using "pass by reference". However, numerical values and Strings are all immutable in place. You cannot change the value of a passed in variable and see that value change in the caller. Dictionaries and Lists on the other hand are mutable, and changes made to them by a called function will be preserved when the function returns. This behavior is confusing and can lead to common mistakes where lists are accidentally modified when they shouldn't be. However, there are many reasons for this behavior, such as saving memory when dealing with large sets.
Ratkaisuna on käyttää esimerkiksi lista-tietotyyppiä tai määritellä muuttuja globaaliksi.
Miten muuttuja määritetään globaaliksi? Laitoin ennen funktiota global kierros, mutta ei vaikuta yhtään mitään.
Eiköhän tuossa ristinolla pelissä nyt kuitenkin riitä peliruudukon napeille yksi funktio, mikä käsittelee painalluksen ja ottaa parametrikseen napin sijainnin ruudukossa.
Eikös lambda ole vain "nimetön" funktio ja joku allaolevan tapainen riitä linkitsemään toiminteen napille, suoritettuna nappien luonti silmukassa? :
napit[i][j] = Button(kehykset[i], text="Tyhjä", command=lambda: ristinolla(i, j))
Kiitos hienojen hakusanojen (Python global argument), sain toimimaan tämän koodini. Katsotaan kun jään seuraavaan vaiheeseen jumiin...
def funktio(nappula, kierrokset): global kierros print "Nappula", nappula kierros = kierrokset + 1 print "Kierros:", kierros, "\n" while True: nappi = raw_input("Nappula: ") funktio(nappi, kierros)
jalski: Tuossa nimettömään funktioon eivät tule i:n ja j:n arvot vaan viittaukset muuttujiin, joten jokainen nappi saa täsmälleen saman toiminnon. Nimetön funktio pitää luoda toisella funktiolla. Kirjoitin tuosta oikein esimerkin pari viestiä sitten, mutta se taisi olla ilman selityksiä liian vaikea hahmottaa.
Metabolix kirjoitti:
jalski: Tuossa nimettömään funktioon eivät tule i:n ja j:n arvot vaan viittaukset muuttujiin, joten jokainen nappi saa täsmälleen saman toiminnon. Nimetön funktio pitää luoda toisella funktiolla. Kirjoitin tuosta oikein esimerkin pari viestiä sitten, mutta se taisi olla ilman selityksiä liian vaikea hahmottaa.
Nopeasti dokumentaatiota lukaisten käsitin hiukan toisin. Mielestäni, jos command määrittelylle antaa pelkän callback-funktion nimen ilman sulkeita tai parametrejä, niin Python ei suorita funktiota ja sijoita funktion paluuarvoa callback-funktioksi vaan määrittää sen suoraan.
Eikös allaoleva määrittele vain nimettömän funktion, mikä kutsuu oikeata callback-funktiota oikeilla parametreillä:
lambda: ristinolla(i, j)
Ei.
def f(x): print x arvo = 1 a = lambda: f(arvo) arvo = 2 b = lambda: f(arvo) arvo = 3 a() # 3 b() # 3
Tuon takia pitää palauttaa se funktio toisesta funktiosta, kuten aiemmin sanoin. Silloin siihen sidotaan funktion (väliaikaiset) parametrit eikä globaaleja muuttujia.
Auts, mutta kokeilepa allaolevaa:
def f(x): print x arvo = 1 a = lambda arvo = arvo: f(arvo) arvo = 2 b = lambda arvo = arvo: f(arvo) arvo = 3 a() # 1 b() # 2
jalski kirjoitti:
napit[i][j] = Button(kehykset[i], text="Tyhjä", command=lambda: ristinolla(i, j))
Ei, ei tuo edelleenkään toimi.
Omankin esimerkkisi perusteella toimiva versio menisi näin:
napit[i][j] = Button(kehykset[i], text="Tyhjä", command=lambda i=i, j=j: ristinolla(i, j))
Minusta ei kuitenkaan ole kovin tyylikästä käyttää oletusargumentteja tuohon, vaan siistimpi ratkaisu olisi tämä:
def ristinolla_cb(i, j): return lambda: ristinolla(i, j) napit[i][j] = Button(kehykset[i], text = "Tyhjä", command = ristinolla_cb(i, j))
(Edit: jalski näköjään muokkasi jo viestiään. Niin minäkin. :))
Metabolix kirjoitti:
Ei, ei tuo edelleenkään toimi. (Mikset kokeile ennen ehdottelua? Edit: Muokkasitkin näköjään viestiäsi.)
Copy-pastasin ylhäältä ja unohdin muokata vahingossa... Otin sitten pois, kun ajattelin, että tuosta näkee muutenkin mitä pitäisi muokata.
Täytyy kyllä sanoa, että mielestäni Infernon Tk-toteutuksen tapa käyttää kanavia ja merkkijonoja on Python toteutusta joustavampi ja helpompi käyttää.
Alla esimerkkinä Limbo -ohjelma, mikä piirtää nappiruudukon, laittaa napin painalluksella X:n tai O:n vuoronperään ruudukkoon ja kirjoittaa stardard outputtiin painetun napin koordinaatit ruudukossa:
implement Testi; include "sys.m"; sys: Sys; Dir: import sys; include "draw.m"; draw: Draw; Screen, Display, Image, Context, Point, Rect: import draw; include "tk.m"; tk: Tk; Toplevel: import tk; include "tkclient.m"; tkclient: Tkclient; Hide: import tkclient; Testi : module { init: fn(ctxt: ref Draw->Context, argv: list of string); }; WinBut : con Hide; BoardSize: con 10; ctxt: ref Draw->Context; main : ref Tk->Toplevel; init(xctxt: ref Draw->Context, nil: list of string) { sys = load Sys Sys->PATH; if (xctxt == nil) { sys->fprint(sys->fildes(2), "testi: no window context\n"); raise "fail:bad context"; } ctxt = xctxt; draw = load Draw Draw->PATH; tk = load Tk Tk->PATH; tkclient= load Tkclient Tkclient->PATH; sys->pctl(Sys->NEWPGRP, nil); tkclient->init(); (t, wmctl) := tkclient->toplevel(ctxt, nil, "Testi", WinBut); if(t == nil) { sys->fprint(sys->fildes(2), "testi: creation of toplevel window failed\n"); raise "fail:creation of toplevel window failed"; } main = t; cmd := chan of string; tk->namechan(main, cmd, "cmd"); display_board(); (current, last) := ("X", "O"); center(main); tkclient->onscreen(main, "exact"); tkclient->startinput(main, "kbd"::"ptr"::nil); for(;;) alt { s := <-main.ctxt.kbd => tk->keyboard(main, s); s := <-main.ctxt.ptr => tk->pointer(main, *s); s := <-wmctl or s = <-main.ctxt.ctl or s = <-main.wreq => tkclient->wmctl(main, s); # täällä käsitellään tiedot pelilaudan painalluksista bcmd := <-cmd => (nil, tokens) := sys->tokenize(bcmd, " "); case hd tokens { "b" => x := int hd tl tokens; y := int hd tl tl tokens; sys ->print("Board( %d, %d ) pressed\n", x, y); tk->cmd(main, sys->sprint(".f%d.b%d configure -text {%s}", x, y, current)); tk->cmd(main, sys->sprint(".f%d.b%d configure -state disabled", x, y)); tk->cmd(main, "update"); (current, last) = (last, current); } } } center(t: ref Tk->Toplevel) { org: Point; ir := tk->rect(t, ".", Tk->Border|Tk->Required); org.x = t.screenr.dx() / 2 - ir.dx() / 2; org.y = t.screenr.dy() / 2 - ir.dy() / 2; if (org.y < 0) org.y = 0; tk->cmd(t, ". configure -x " + string org.x + " -y " + string org.y); } display_board() { i, j: int; pack: string; tk->cmd(main, "frame .f"); tk->cmd(main, "pack .f -fill x"); for (i = 1; i <= BoardSize; i++) { tk->cmd(main, sys->sprint("frame .f%d", i)); pack = ""; for (j = 1; j <= BoardSize; j++) { pack += sys->sprint(" .f%d.b%d", i, j); tk->cmd(main, sys->sprint("button .f%d.b%d -text { } -width 14 -command {send cmd b %d %d}", i, j, i, j)); } tk->cmd(main, sys->sprint("pack %s -side left", pack)); tk->cmd(main, sys->sprint("pack .f%d -side top -fill x", i)); } tk->cmd(main, "update"); }
Mielipiteesi ehkä johtuu siitä, että osaat Limboa paljon paremmin kuin Pythonia. Minusta tästä taas tulee Pythonilla paljon selkeämpi. (Ohjelma ei ole täsmälleen sama, mutta kyllä siinä on nappeja ja ristinolla tarkistuksineen.)
#!/usr/bin/python # -*- coding: UTF-8 -*- import Tkinter def kaynnista(): global kierros, napit, ruudut, ruutuja ikkuna = Tkinter.Tk() kierros = 0 napit = {} ruudut = {} for y in range(0, ruutuja): kehys = Tkinter.Frame(ikkuna) kehys.pack() for x in range(0, ruutuja): napit[(x, y)] = Tkinter.Button(kehys, command = painettu_cb(x, y), width = 1) napit[(x, y)].pack(side = Tkinter.LEFT) Tkinter.mainloop() def painettu_cb(x, y): return lambda: painettu(x, y) def painettu(x, y): global kierros, napit, ruudut ruudut[(x, y)] = 'XO'[kierros % 2] print("Ruutu (%d, %d) on nyt %c." % (x, y, ruudut[(x, y)])) napit[(x, y)].configure(text = ruudut[(x, y)], command = False) kierros = kierros + 1 if tarkista_voitto(x, y, ruudut[(x, y)]): for nappi in napit.itervalues(): nappi.configure(command = False) print("Peli loppui, voittaja on %c." % (ruudut[(x, y)])) def tarkista_voitto(x, y, m): global voittopituus # Ruutu (x, y) lasketaan kahdesti, joten vertailuna > eikä >=. if (pituus(x, y, 0, -1, m) + pituus(x, y, 0, 1, m) > voittopituus): return True if (pituus(x, y, -1, -1, m) + pituus(x, y, 1, 1, m) > voittopituus): return True if (pituus(x, y, -1, 0, m) + pituus(x, y, 1, 0, m) > voittopituus): return True if (pituus(x, y, 1, -1, m) + pituus(x, y, -1, 1, m) > voittopituus): return True return False def pituus(x, y, dx, dy, merkki): global ruudut lkm = 0 while ruudut.has_key((x, y)) and ruudut[(x, y)] == merkki: x, y, lkm = x + dx, y + dy, lkm + 1 return lkm ruutuja, voittopituus = 15, 5 kaynnista()
(Mahtavaa, ensimmäinen millään mittapuulla kunnollinen Tk-ohjelmani.)
Edit. Lyhensin koodia vielä vähän.
Hyvä Metabolix, tuon kannalta saan helpommin tehtyä oman pelini. Hienoa, että täällä on näin taitavaa porukkaa. Jos osaisin yhtä hyvin ohjelmoida, niin olisin hyvin onnellinen. Toivottavasti sinä olet. =)
Hyöty on tässä ketjussa ehkä hieman kyseenalainen, mutta Pythonin esittelyn kannalta näytän toisenkin toteutustavan Metabolixin tarkista_voitto-funktiolle.
def tarkista_voitto(x, y, m): suunnat = ((0, 1), (1, 1), (1, 0), (-1, 1)) def merkkeja_voittoon(sx, sy): # Ruutu (x, y) lasketaan kahdesti, joten vertailuna > eikä >=. return pituus(x, y, sx, sy, m) + pituus(x, y, -sx, -sy, m) > voittopituus return any(merkkeja_voittoon(sx, sy) for sx, sy in suunnat)
Erotin alkuperäisistä if-lauseista poikkeavat kohdat tupleksi (suunnat). tarkista_voitto-funktio palauttaa True, jos jossakin (any) suunnassa on tarpeeksi merkkejä voittoon, muuten False. Irrotin selkeyden vuoksi tarkemmat yksityiskohdat paikalliseen funktioon. Koska voittopituutta ei muuteta funktiossa, global-mainintaa ei tarvita. Globaalia arvoa käytetään automaattisesti, koska samalla nimellä ei ole lokaalia muuttujaa.
Any-funktion argumenttina oleva "...for...in..." -rakennelma on nimeltään generaattorilauseke.
Pahoittelen jos sotken itse asiaa :) Tätä ei tarvitse ymmärtää ristinollapeliä varten.
Infernon kanavatoteutuksen joustavuudella tarkoitin sitä, että ikkunatoimintojen käsittely silmukka voi toimia omassa säikeessään, pelitapahtumien käsittely silmukka omassaan (=pelin päälooppi) ja mahdollisen toisen pelaajan komennot verkon yli saa välitettyä pelille käytännössä ilman muutoksia pelitapahtumien käsittely logiikkaan. Kaikki tämä toimii synkronoituna kanavien avulla.
Macro:
Tästä ristinollasta tulisi hyvä kahden pelaajan client-server peli. On todennäköisesti vielä helpompi toteuttaa, kuin kehittää kakkospelaajalle hyvin toimiva tekoäly (=random-älyä ei lasketa hyväksi). Metabolixin hyvän Tk-esimerkin pohjalta pääset varmasti eteenpäin.
jalski: Tämä oli alunperin tarkoituskin, tehdä siis kahden pelattava "nettipeli". Ajattelin vain, että kun en ole mitään peliä koskaan tehnyt, niin on helpompi aloittaa sen tekeminen varovasti. Client-server tyylillä sitä ei ole välttämättä hankala toteuttaa, mutta miten tästä tehtäisiin client-client, että ei tarvitse pelistä kahta eri versiota?
Muokkaus. Miten voisin tyhjentää kaikki framet ja siirtyä koodin alkuun? Niinkuin käynnistäisi ohjelman uudelleen.
Macro kirjoitti:
Client-server tyylillä sitä ei ole välttämättä hankala toteuttaa, mutta miten tästä tehtäisiin client-client, että ei tarvitse pelistä kahta eri versiota?
Voit sisällyttää ohjelmaan molemmat sekä serverin, että asiakkaan ja ottaa komentoriviltä parametriksi toimitaanko serverinä vai asiakkaana. Sitten vain toimit valinnan mukaan. Tämä onnistunee muutamalla ehtolauseella.
Toinen vaihtoehto on erillisen serverin tekeminen, mikä palvelee kahta asiakasta ja toimii välittäjänä asiakkaiden välillä.
Macro kirjoitti:
Miten voisin tyhjentää kaikki framet ja siirtyä koodin alkuun? Niinkuin käynnistäisi ohjelman uudelleen.
Ei kai niitä kaikkia kehyksiä tyhjentää tarvi? Riittää, kun määrität kaikille napeille tyhjän label tekstin tai kuvan ja asetat takaisin aktiivisiksi, jos olet disabloinnut ne välttääksesi turhia painallus tapahtumia. Tietysti muista nollata myös muut pelin Tk:hon liittymättömät tietorakenteet ja muuttujat.
Niin juu... Empäs ajatellut tarpeeksi yksinkertaisesti. Miten asetan nappuloiden oletusväri? Gray on liian tumma ja white valkoinen.
Muokkaus. #ECE9D8 on oikea väri, mutta kun laitan sen configuressa taustaväriksi, niin viivojen väri tummenee samalla. Miten saisin palautettua ne normaaliin tilaan siis?
Älä turhaan hirveästi tappele vielä tässä vaiheessa nappuloiden värin tai muun kosmeettisen kanssa. Keskity saamaan ohjelman logiikka kuntoon.
Innostuin muuten itsekin tekemään ristinolla pelistä omaa versiota Infernolle Limbolla. Tällä hetkellä puuttuu vielä toinen pelaaja ja tarkistukset pelialueella.
Yhtenä esimerkkinä Infernolla kanavien käytöstä pelin toteutuksessa:
Molemmilla pelaajilla on oma kanavansa, mihin tulevia viestejä käsitellään pelin pääloopissa. Ongelmana on miten estää viestien käsittely muulloin, kuin pelaajan omalla vuorolla pelaajan painellessa peliruudukon nappeja?
Ratkaisu on yksinkertainen:
- määritellään pelaajille apukanavat ja dummy-kanava esim. :
p1ch := p1cmdchan; # p1cmdchan on kanava, mihin tulevat pelaajan viestit dummypch := chan of string; # dummy-kanava p2ch := dummyp2ch; # ykköspelaaja aloittaa
Peliloopissa käsitellään pelaajien viestit, mitkä tulevat p1cmdchan -ja p2cmdchan -kanaviin.
... p1cmd := <-p1ch => p1ch = dummypch; (nil, tokens) := sys->tokenize(p1cmd, " "); case hd tokens { "b" => x := int hd tl tokens; y := int hd tl tl tokens; # tähän tulisi viestin käsittely } absorb(p2cmdch); # poista odottava blokannut viesti p2ch = p2cmdch; # anna vuoro kakkospelaajalle p2cmd := <-p2ch => p2ch = dummypch; (nil, tokens) := sys->tokenize(p2cmd, " "); case hd tokens { "b" => x := int hd tl tokens; y := int hd tl tl tokens; # tähän tulisi viestin käsittely } absorb(p1cmdch); # poista odottava blokannut viesti p2ch = p1cmdch; # anna vuoro ykköspelaajalle ...
Tässä vielä koodi absorb-funktiolle:
absorb(ch : chan of string) { for(;;) { alt { <- ch => ; * => return; } } }
Olet oikeassa, ei kannata tehdä mitään hienouksia ennen kuin ohjelma toimii muuten kunnolla.
# -*- coding: UTF-8 -*- #Lisätään ohjelmaan headerit from Tkinter import * import tkSimpleDialog import tkMessageBox root = Tk() root.title("Ristinolla") #Luetaan voittotiedot def tilastot(): global tiedot try: tiedosto = open("C:\\Program Files\\MPS\\Ristinolla\\tilastot.txt", "r") tiedot = tiedosto.readlines() tiedosto.close() except IOError: print "Tilastot.txt tiedostoa ei löytynyt" tilastot() #Pelin käynnistysfunktio def kaynnista_peli(): #Laitetaan muuttujat/taulukot globaaleiksi, jotta nämä näkyvät muuallakin kuin tässä funktiossa global kierros, napit, ruudut, ruutuja, root #Alustetaan root #root = Tk() #Alustetaan kierros, napit ja ruudut kierros = 0 napit = {} ruudut = {} root.minsize(width=300, height=300) #Luodaan napit ja ruudut #Käydään napit pystysuunnassa for y in range(0, ruutuja): kehys = Frame(root) kehys.pack() #Käydään napit vaakasuunnassa for x in range(0, ruutuja): napit[(x, y)] = Button(kehys, text = "", command = nappula_painettu(x, y), width = 2) napit[(x, y)].pack(side = LEFT) #Käynnistetään mainloop() #Nappulan koordinaatit def nappula_painettu(x, y): return lambda: painettu(x, y) #Tulostetaan tilanne def tilanne(pelaaja): if pelaaja == "X": pisteet = tiedot[1] else: pisteet = tiedot[0].replace("\n", "") kehys = Frame(root) kehys.pack() teksti = Button(kehys, text=pelaaja + ": " + pisteet + " voittoa", borderwidth=0, command="", state=DISABLED) teksti.pack(side=LEFT) #Näitä nappuloita on jo painettu nappulat = [] #Tällä merkitään ristit/nollat def painettu(x, y): #Asetetaan muuttujien näkyvyysalueet globaaleiksi global kierros, napit, ruudut, voitto #Jos kierros % 2 == 0, niin on nollan vuoro ruudut[(x, y)] = "OX"[kierros % 2] #print "Ruutu (%d, %d) on nyt %c" % (x, y, ruudut[(x, y)]) #Asetetaan nappulan nimeksi uusi tieto, ja komennoksi false napit[(x, y)].configure(text = ruudut[(x, y)], state = DISABLED) #Lisätään kierroksia yhdellä kierros += 1 #Tarkistetaan voitto if tarkista_voitto(x, y, ruudut[(x, y)]): #Disabloidaan kaikkien nappien komennot tyhjiksi for nappi in napit.itervalues(): nappi.configure(text = "", width = 2, state = NORMAL) print "Peli loppui, %c voitti!" % (ruudut[(x, y)]) tallenna("%c" % (ruudut[(x, y)])) voitto = True else: voitto = False def tarkista_voitto(x, y, m): #Laitetaan muuttuja globaaliksi global voittopituus #Tarkistetaan if (pituus(x, y, 0, -1, m) + pituus(x, y, 0, 1, m) > voittopituus): return True if (pituus(x, y, -1, -1, m) + pituus(x, y, 1, 1, m) > voittopituus): return True if (pituus(x, y, -1, 0, m) + pituus(x, y, 1, 0, m) > voittopituus): return True if (pituus(x, y, 1, -1, m) + pituus(x, y, -1, 1, m) > voittopituus): return True return False def pituus(x, y, dx, dy, merkki): #Globaaliksi muuttuja global ruudut lukema = 0 while ruudut.has_key((x, y)) and ruudut[(x, y)] == merkki: x, y, lukema = x + dx, y + dy, lukema + 1 return lukema def tallenna(merkki): global x_tilanne, o_tilanne x_tilanne = int(tiedot[1]) o_tilanne = int(tiedot[0].replace("\n", "")) if merkki == "O": o_tilanne += 1 teksti.configure(text="O: " + str(o_tilanne) + " voittoa") else: x_tilanne += 1 teksti2.configure(text="X: " + str(x_tilanne) + " voittoa") tiedosto = open("c:\\Program Files\\MPS\\Ristinolla\\tilastot.txt", "w") tiedosto.write(str(o_tilanne) + "\n" + str(x_tilanne)) tiedosto.close() #Tulostetaan tilanne #Ruma tapa, korjaan kyllä =) kehys = Frame(root) kehys.pack() teksti = Button(kehys, text="O: " + tiedot[0].replace("\n", "") + " voittoa", borderwidth=0, command="", state=DISABLED) teksti.pack(side=LEFT) kehys2 = Frame(root) kehys2.pack() teksti2 = Button(kehys2, text="X: " + tiedot[1] + " voittoa", borderwidth=0, command="", state=DISABLED) teksti2.pack(side=LEFT) ruutuja, voittopituus = 10, 5 kaynnista_peli()
Tältä näyttää tämän hetkinen ohjelmani. Siinä on jo jotain "hienouksia", kuten pisteiden kirjaus tiedostoon ja sieltä niiden tulostaminen. Ongelmana on, että jokin taulukko/muuttuja ei tyhjenny/tyhjennetä pelin lopussa, niin pelissä on näkymättömiä rasteja ja nollia. Siitä tulee harmeja, kun sopivasti onnistuu painamaan ruudulle. Keksittekö mitkä tarpeellista muuttujaa/listaa ei tyhjennetä? Minä en sitä löytänyt.
Ongelmana on se, että jos jossakin kohdassa on ollut 4 peräkkäistä rastia, ja laitetaan yksi rasti niin että tulisi 5 putkeen, niin pelaaja saa siitä pisteen!
Muuten, miten saan ohjelmassa toimimaan läpinäkyvän ikonin? Laitoin valkoisen värin läpinäkyväksi, mutta nyt se näkyy ohjelmassa oranssina.
Oliko edelliset kysymykset epäselviä, pitääkö selventää? =)
Mihin tarvitset ohjelmassa läpinäkyvyyttä?
Oma ristinolla toteutukseni rupeaa pikkuhiljaa hahmottumaan. Pelilaudan tarkistukset puuttuvat edelleen, mutta verkkopeli toimii. Toteutin tosin vasta serverin osuuden pelistä, mutta testasin toiminnan lähettämällä kakkospelaajan ohjausviestejä telnetillä ykköspelaajan serverille.
Laitan pelin valikkoon mahdollisuuden isännöidä peliä tai toimia asiakkaana, teen pelilaudan tarkistukset ja siistin koodia hiukan.
Olisi hauska nähdä muiden tekemiä ristinolla verkkopelitoteutuksia eri ohjelmointikielillä ja vertailla erilaisia ratkaisuja.
Sekin on sellainen lisäominaisuus, voisi pohtia vasta kun on muuten valmis.
Muokkaus. Nyt sitten ei ole yhtään virheitä enään pelissä, vielä se nettipeli osuus pitää saada valmiiksi. =)
En tiedä Pythonin Tk-toteutuksesta, mutta Infernon toteutuksessa voi käyttää panel widgettiä ja tähän saa kopioitua imagen Tk:n putimage -funktiolla. Imageen voi tietysti, piirtää käyttäen Draw-moduulia hyväkseen.
Joko olet ehtinyt toteuttamaan verkkopeliä?
Oma toteutukseni rupeaa olemaan loppusuoralla. Muutama dialogi-ikkuna on toteuttamatta ja joitain mahdollisia virhetilanteita vielä käsittelemättä.
Peli ei kyllä taida kauneuskilpailuja voittaa, mutta ajanee asiansa. Alla löytyypi kuva kehitysversiosta. Kuvassa kakkospelaajan komennot lähetetään telnetillä.
En ole tänään ehtinyt sitä paljon miettiä, kun pitää tehdä esitelmä kouluun. Mutta juu, yhdellä koneella pelaus onnistuu suoraan sanoen mainiosti.
Peliin toteutettu mahdollisuus isännöidä peliä tai liittyä peliin. Oikeastaan enää puuttuu voittosuoran korostaminen eri värillä ja peliin liityttäessä dialogi-ikkuna, mihin syötetään isäntäkoneen osoite.
Infernon Limbo toteutuksessa koodia tällä hetkellä n. 500 väljää riviä ja valmis tuotos mahtunee n. 700 riviin.
Selevä. =) Tässä kun vihdoin aloitin verkkopeliversiota tekemään, niin jäi yksi asia päähän. Miten saisin valikkoon sellaisen ominaisuuden, että kun painetaan jotakin menua niin tulisi normaalisti se valikko, mutta jossain kohdassa olisi vielä lisävalintoja (Aukeaisi seuraava sivulle) jossa voisi valita yhden vaihdoehdon. En tiedä, että selitinkö järkevästi. Kuitenkin varmaan ymmärrätte tarkoituksen.
Muokkaus. Cascading menu-hakusanoilla löytyy jotain, katsompas...
No nyt löytyikin, mutta yksi ongelma ilmenee:
def valikko1(): valikko1 = Menubutton(root, text = 'Ristinolla') valikko1.pack(side = LEFT) valikko1.menu = Menu(valikko1, tearoff = 0) valikko1.menu.valikko12 = Menu(valikko1.menu, tearoff = 0) valikko1.menu.valikko12.add_radiobutton(label = "Valitse vastustaja", state = DISABLED) valikko1.menu.valikko12.add_separator() for kaveri in kaverit: valikko1.menu.valikko12.add_radiobutton(label = kaveri, menu = valikko1.menu.valikko12.kaveri) valikko1.menu.valikko12.kaveri = Menu(valikko1.menu.valikko12, tearoff = 0) valikko1.menu.valikko12.kaveri.add_cascade(label = "Testi 1") valikko1.menu.valikko12.kaveri.add_cascade(label = "Testi 2") if len(kaverit) > 0: valikko1.menu.valikko12.add_separator() else: valikko1.menu.valikko12.add_radiobutton(label = "(Ei kavereita)", state = DISABLED) valikko1.menu.valikko12.add_separator() valikko1.menu.valikko12.add_cascade(label = 'Uusi pelikaveri', command = uusi_kaveri) valikko1.menu.add_cascade(label = 'Uusi peli') valikko1.menu.add_cascade(label = 'Yhdistä', menu = valikko1.menu.valikko12) valikko1.menu.add_separator() valikko1.menu.add_cascade(label = 'Lopeta') valikko1['menu'] = valikko1.menu return valikko1
Tällä tyylillä koittaisin tehdä valikon. Ongelmaksi tulee tuo for-silmukka. Ohjelman alussa on funktio, joka lukee tiedostosta kaverit.txt kaikki kaverit ja asettaa ne taulukkoon. Sitten pitäisi generoida automaattisesti tuonne valikkoon kohdat Testi 1 ja Testi 2.
Jos kavereita ei ole, valikon pitäisi näyttää tältä:
Ristinolla Uusi peli Yhdistä -> (Valitse kaveri) ---------- (Ei pelikavereita) Lopeta ------------------ Lisää uusi kaveri
Jos taas on kavereita vaikka kolme kappaletta, niin se olisi tälläinen:
Ristinolla Uusi peli Yhdistä -> (Valitse kaveri) ---------- Pekka Lopeta Maija Antti ----------------- Lisää uusi kaveri
Koodi generoi ensimmäiset valikot hyvin, mutta kun pitäisi kavereiden kohdalle tehdä tuollainen uusi valikko, niin tulee virheilmoitus. Miten korjaan tuon for-silmukan siten, että myös jälkimmäinen valikko (testi 1, testi2) tulee jokaisen pelaajan kohdalle? Nyt saan virheilmoitukset AttributeError: Menu instance has no attribute 'kaveri'
Tuon menu systeemisi rakennehan on alla olevan mukainen:
Ristinolla (menubutton) Uusipeli (command) Yhdistä (cascade) --> (Valitse kaveri) (command) separator Pekka (command) Lopeta (command) Maija (command) Antti (command) separator Lisää uusi kaveri (command)
Eli menubuttoniin on linkitetty menu, missä on: Uusipeli, Yhdistä, separator ja Lopeta. Yhdistä on cascade-tyyppinen ja siihen on linkitetty menu, missä on lista kavereista.
Kun tuota katsot, niin sen rakentaminen aika helppo jakaa vaiheisiin. Tuo kaverit menuhan on oikeastaan ainoa, mikä pitää muodostaa silmukassa dynaamisesti.
Termi valikko1.menu.valikko12.kaveri ei varmastikaan toimi, vaan oikea jäsen pitäisi hakea jollain funktiolla, joka ottaisi parametrikseen kaveri-muuttujan arvon. (Tuossa yrität hakea kirjaimellisesti jäsentä "kaveri".) Tähän varmaan löytyy dokumentaatiosta ratkaisu.
Koitin etsiä dokumentaatiosta neuvoa, mutta ei suoraan sanoen löytynyt mitään. Koitin myös tuota Metabolixin neuvoa, mutta en keksinyt mitä tarkoitit suoraan.
Tarkoitin siis, että valikko1.menu.valikko12.kaveri hakee jäsentä nimeltä "kaveri", kun pitäisi hakea esimerkiksi jäsentä "Uolevi". Jäsen pitäisi siis hakea jollain funktiolla: valikko1.menu.valikko12.hae_jasen(kaveri). Mutta unohda tämä.
Nyt kun tarkemmin katson, niin eihän tuossa koko kaverisilmukassa ole järkeä. Yrität käyttää arvoa valikko1.menu.valikko12.kaveri jo ensimmäisellä rivillä, vaikka asetat sen vasta seuraavalla. Lisäksi teet siitä valikon, vaikka eikö jokainen kaveri pitäisi lisätä yksinkertaisesti samalla tavalla kuin tuo ("Ei kavereita)"?
Seuraavaa ehtolausetta voisi myös yksinkertaistaa, kun molemmissa tapauksissa kutsutaan lopuksi add_separator.
Edit: Ainakin näin sain luotua mielestäni kelvollisen valikon:
from Tkinter import * root = Tk() nappi = Menubutton(root, text = "Ristinolla") nappi.pack() valikko = Menu(nappi, tearoff = 0) kaverivalikko = Menu(valikko, tearoff = 0) nappi["menu"] = valikko valikko.add_command(label = "Yhdistä") valikko.add_cascade(label = "Kaveri", menu = kaverivalikko) kaverivalikko.add_command(label = "Pera") kaverivalikko.add_command(label = "Tipsu") kaverivalikko.add_separator() kaverivalikko.add_command(label = "Uusi kaveri")
Muuta vain kaverien lisäämisen silmukaksi ja lisää toiminnot.
Pitäisikö funktion ottaa arvoksi kaverin nimi ja palauttaa se?
Koitin näin:
friends = [] friends.append("Mauri") friends.append("Martti") def nimi(pelaaja): return pelaaja for friend in friends: kaverivalikko.add_cascade(label = friend, menu = nimi(friend)) nimi(friend).add_command(label = "Pelaa") nimi(friend).add_command(label = "Poista kaveri")
Tästä saan vain virheilmoituksen, että str-objektilla ei ole add_command attribuuttia. Nyt olen kuitenkin lähempänä oikeaa tulosta, eikö vain?
Eihän tuossa funktiossa ole mitään järkeä, koska se palauttaa parametrinsa suoraan, eikä tee mitään muutoksia siihen.
def nimi(pelaaja): return pelaaja #esim nämä kaksi ajavat saman asian joku = nimi(pelaaja) joku = pelaaja
Vehkiksen viestiin liittyen, tämä:
nimi(friend).add_command(label = "Pelaa")
...on arvolla "Mauri" sama kuin tämä:
"Mauri".add_command(label = "Pelaa")
Ja merkkijonolla (tässä "Mauri") ei ole metodia add_command.
En ole vuosiin koodannut Tkinterillä, joten en osaa äkkiseltään kertoa oikeaa koodia, mutta tämä näyttäisi hyvältä esimerkiltä: http://effbot.org/tkinterbook/menu.htm
Noista nimistä voi päätellä miten alavalikko ensin rakennetaan ja sitten kiinnitetään muuhun menuun.
Ei tuolla sivulla neuvottu mitään tähän ongelmaan. Uusia neuvoja?
Sait jo aiemmin koodin, joka liittää napinpainallukseen tietyn ruudun (koordinaatit). Samaa voit soveltaa tässäkin. Kannattaa käyttää nimen sijaan kaverin indeksiä listassa.
Yrität tunkea liikaa asioita Tk:n olioihin. Kaikki käy helpommin, kun säilytät kavereiden valikot erikseen.
kaverivalikot = [] for i in range(0, len(kaverit)): nimi = kaverit[i] valikko = Menu(...) kaverivalikot.append(valikko)
Mutta, tästä tulee taas se str-ongelma. Onnistun vaan muodostamaan tästä "kaveri".add_cascade(...).
Laita valikko.add_cascade
. Tässähän on tarkoitus luoda joka kaverille uusi valikko, joka sitten lisätään vielä kaverivalikot-listaan siltä varalta, että sitä halutaan myöhemmin muokata.
Voitko näyttää? Ei onnistu nähtävästi, on tullut kohta kaksi tuntia tapeltua sen kanssa. Saan koko ajan sitä str-erroria, vaikka mitä tekisin.
Oma ristinollan Limbo toteutukseni Infernolle on nyt valmis. Julkaisen lähdekoodin täällä, kunhan saan sen siistittyä julkaisukuntoon.
Macro:
Kehotan edelleen keskittymään itse peliin ja jättämään hienot valikot ja muut loppuun, kun pelin runko on testattu toimivaksi. Edistyminen projektissa on näin nopeampaa ja mielenkiinto ei lopahda kesken kaiken valikkojen kanssa tapellessa.
Alla joitain kuvia omasta toteutuksestani:
http://www.tip9ug.jp/who/jalih/rn1.jpg
http://www.tip9ug.jp/who/jalih/rn2.jpg
http://www.tip9ug.jp/who/jalih/rn3.jpg
No, voisihan sen niinkin tehdä, mutta nyt ei juuri kiinnosta tehdä nettipeliversiota siitä. Kun saan tuon valikon kuntoon, niin katsotaan jaksanko alkaa toteuttaa nettiversiota.
En ole aivan kärryillä mitä noista menukohdista halutaan tapahtuvan, mutta tässä sellaisenaan toimiva esimerkki funktiokutsuineen:
#!/usr/bin/python # -*- coding: utf-8 -*- from Tkinter import * def valitse_kaveri(kaveri): # tee jotain fiksua tässä print "kaveriksi valittiin", kaveri def lisaa_uusi_kaveri(): # uuden kaverin lisäys pass kaverit = ["Mauri", "Matti"] root = Tk() nappi = Menubutton(root, text="Ristinolla") nappi.pack() valikko = Menu(nappi, tearoff=0) kaverivalikko = Menu(valikko, tearoff=0) nappi["menu"] = valikko valikko.add_command(label="Uusi peli") valikko.add_cascade(label="Yhdistä", menu=kaverivalikko) for kaveri in kaverit: kaverivalikko.add_command(label=kaveri, command=lambda a=kaveri:valitse_kaveri(a)) kaverivalikko.add_separator() kaverivalikko.add_command(label="Lisää uusi kaveri", command=lisaa_uusi_kaveri) root.mainloop()
Muokkaus. Kiitos Chiman, sain koodistasi vinkin miten toteutan valikon. =)
Ehdit näköjään muokata jo viestiäsi, mutta siinä olleeseen rakenteeseen sopii tämä for-silmukka. Koodi muilta kohdilta sama kuin edellisessä viestissäni.
for kaveri in kaverit: kaveritoiminnot = Menu(kaverivalikko, tearoff=0) kaveritoiminnot.add_command(label="Poista", command=lambda a=kaveri:poista_kaveri(a)) kaveritoiminnot.add_command(label="Yhdistä", command=lambda a=kaveri:yhdista_kaveri(a)) kaverivalikko.add_cascade(label=kaveri, menu=kaveritoiminnot)
Jep, tajusin tämän melkein samantien, kun suoritin ohjelmasi. =) Helppoja asioita on vaikea keksiä. :D
Viidesti olen mukamas keksinyt, että miten saan saman ohjelman toimimaan ristinollapelissä verkossa. En millään keksi logiikkaa, että miten saan odotettua että toinen laittaa oman vuoronsa.
Metabolix, oletko tehnyt oman version tästä nettipelinä Pythonilla? Kiinnostaisi tietää, että miten teillä menee vastustajan siirron odotus ja vastaanottaminen. Se onkin se ainoa ja vaikein osuus siinä nettipelin teossa. En saa päähäni, että millaisen silmukan teen.
Pistä vaikka joku globaali-muuttuja, mikä ilmoittaa kumman vuoro on. Pelilaudan nappien callback-aliohjelmassa tarkistat alussa muuttujasta kumman vuoro on ja jos on oma vuoro, niin käsittelet painalluksen ja lopuksi vaihdat muuttujan tilaksi kaverin vuoron. Jos taas vuoro ei ole oma painalluksen tullessa, niin et tee mitään.
Postitin koodivinkkeihin oman Infernon Limbo toteutukseni ristinollasta. Jos tuo joskus hyväksytään koodivinkkeihin, niin voit ehkä saada siitä joitain ideoita.
Niin minulla olikin aluksi (if kierros % 2 == 0: ..., else: ...), mutta en keksi miten saan tehtyä sen lopun. Mitä laitan if-lauseiden jälkeen?
Muokkaus. Kyllähän sitä vinkkiäsi voi lukea. =) Helpottaisi varmasti aika kummasti, jos joku kokenut tekisi Pythonilla oman toteutuksensa malliksi. =)
Edelliseen viestiini viitaten, unohda ne kierrokset.
Esim. serverille: X aloittaa, alusta ohjelman alussa esim. globaali muuttuja vuoro = 1. Nyt ohjelmasi käsittelee paikallisen pelaajan painallukset. Kun painallus on käsitelty ja siirto on hyväksyttävä, niin lopuksi vaihdat muuttujaan 0. Kun kakkospelaajan siirto on käsitelty, sijoitat muuttujaan 1 ja näin annat vuoron takaisin paikalliselle ykköspelaajalle.
Ja pidän tämän kaiken while-silmukassa? Jos joku jaksaa/ehtii, niin mielelläni katsoisin Pythonilla toteutettua versiota.
En osaa nyt suoralta kädeltä antaa vastausta ongelmaasi. Näyttäisi siltä, että joudut nyt hiukan taas opiskelemaan miten sockettien käsittely toteutetaan Tkinterin kanssa.
Tuolla on ainakin jotain keskustelua aiheesta:
http://markmail.org/message/2t5uoke2nf4dxsca#query: page:1 mid:2vyfilwsrhmeduwr state:results
Nyt sain toteutettua toiminnon siten, että yhteyden muodostus toimii. Ongelmana on vielä se, että miten tunnistetaan kumman vuoro on lähettää dataa. Yleensäkkin kun ohjelmoin, niin teen yksinkertaiseteun version siitä koko ohjelmasta, jolloin voin kokeilla sillä niin ettei kaikki asiaan kuulumattomat toiminnot ole siinä mukana. Nytkin on sitten niin. Minulla on muutama funktio:
client(): Ottaa yhteyden serveriin, jos valikosta otetaan client-tila käyttöön.
server(): Ottaa yhteyden clienttiin, jos valikosta otetaan server-tila käyttöön.
laheta(): Lähettää kummalle tahansa paramenttinä annetun datan, riippuen kumpaan on yhdistetty.
vastaanota_serverilta(): Käy silmukassa niin kauan läpi, kunnes saadaan dataa serveriltä.
vastaanota_clientilta(): Pyörittää silmukkaa, kunnes saadaan dataa.
close(): Sulkee yhteyden
chat(): Muodostaa yhteyden
# -*- coding: utf-8 -*- import socket from Tkinter import * import tkSimpleDialog #Alustetaan root root = Tk() #Vuoron vaihto onnistuu tällä #1 = serveri (X), 0 = clientti (O) vuoro = 1 def client(): #Otetaan etäyhteys toiseen koneeseen serveri = ("91.152.133.214", 1337) #Sokketti valmiiksi sokketti = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sokketti.connect(serveri) #Viestin lähetys onnistuu serverille seuraavalla #sokketti.send(lähetettävä_data) def server(): #Otetaan paikallinen yhteys localhostiin serveri = (socket.gethostname(), 1337) #Sokketti valmiiksi sokketti = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sokketti.bind(serveri) #Odotetaan klienttiä sokketti.listen(1) #Hyväksytään yhteys asiakas, osoite = sokketti.accept() #Status... teksti.configure(text = "Yhdistetty!") #Datan lähettäminen: #asiakas.send(lähetettävä_data) def laheta(data): #Lähetetään siirtodata serverille/klientille sokketti.send(data) def vastaanota_serverilta(): #Pyöritetään silmukassa while True: #Vastaanotetaan siirtodata siirto = sokketti.recv(1024) #Jos sitä ei ole, break if not siirto: break else: return siirto vuoro = 0 break def vastaanota_clientilta(): #Pyöritetään silmukassa while True: #Vastaanotetaan siirtodata siirto = asiakas.recv(1024) #Jos sitä ei ole, break if not siirto: break else: #Muuten palautetaan arvo ja break return siirto vuoro = 1 break def close(): try: #Suljetaan yhteys sokketti.close() except: teksti.configure(text = "Virhe sokketin sulkemisessa!") def yhteys(serveri): if serveri == True: #Jos ollaan serveri, niin vastaanotetaan viestit klientiltä teksti.configure(text = "Odota, odotetaan vastustajaa...") server() else: #Ollaan klient, vastaanotetaan serveriltä teksti.configure(text = "Odota, yhdistetään serveriin!") client() teksti.configure(text = "Yhdistetty!") def chat(toiminta): if toiminta == "server": #Toimitaan serverinä #Yhteys muodostetaan asiakkaaseen yhteys(True) palautus = vastaanota_clientilta() teksti.configure(text = "Client sanoo: " + palautus) else: #Toimitaan klienttinä #Yhteys muodostetaan serveriin yhteys(False) palautus = vastaanota_serverilta() teksti.configure(text = "Serveri sanoo: " + palautus) valikko = Menu(root, tearoff = 0) yhteystapa = Menu(valikko, tearoff = 0) valikko.add_cascade(label = "Ristinolla (BETA)", menu = yhteystapa) yhteystapa.add_radiobutton(label = "Isäntäkone", command = lambda: chat("server")) yhteystapa.add_radiobutton(label = "Client", command = lambda: chat("client")) yhteystapa.add_command(label = "Sulje yhteys", command = close) root.config(menu = valikko) teksti = Label(root, text = "Viesti...") teksti.pack(side = LEFT) mainloop()
Vastaanotto funktioissa on jotain vikaa, koska antaa virheen datan vastaanottamisessa. Global name sokketti/asiakas* is not defined
* Riippuu tilasta.
Muokkaus. Tuo on muuten aika buginen testiohjelma.
Kun yhteys on muodostettu, palvelimen täytyy ilmoittaa pelimerkit ja aloittajan (esim. "SX CO X"). Tämän jälkeen silmukka voi molemmilla toimia näin:
kun peli on kesken: jos vuoro == oma: odota nappia lähetä siirto muuten: odota dataa
Jep. Kokeilin ihan komentorivipohjaisesti, että saanko kysyttyä vuorotellen viestiä ja lähetetty & tulostettua sen. Näin sain toteutettua jotain, joka kyllä jää jumiin toiseen while-lauseeseen.
# -*- coding: utf-8 -*- from Tkinter import * import socket tyyli = raw_input("Serveri = s, client = c: ") vuoro = 1 if tyyli == "s": serveri = (socket.gethostname(), 1337) sokketti = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sokketti.bind(serveri) print "Odotetaan asiakasta..." sokketti.listen(1) laheta, osoite = sokketti.accept() print "Yhdistetty!" else: ip = raw_input("Serverin ip-osoite: ") serveri = (ip, 1337) print "Yhdistetään..." laheta = socket.socket(socket.AF_INET, socket.SOCK_STREAM) laheta.connect(serveri) print "Yhdistetty!" if tyyli == "c": vuoro = 2 def mainloop(): global vuoro, tyyli if vuoro % 2 == 0: return "X" else: return "O" vuoro += 1 print vuoro print "Sinä olet " + mainloop() while True: pelaajan_vuoro = mainloop() if pelaajan_vuoro == "X": viesti = raw_input("Viesti: ") laheta.send(viesti) else: while True: try: data = laheta.recv(1024) except: print "Virhe datan vastaanottamisessa!" break if not data: break else: print "Viesti: " + data
Nyt tämä kysyy viestiä vain toiselta (client) koko ajan. Clientti on aina nolla, kun taas serveri on aina x. Eli se kohta toimii. Mitä pitää muokata, että kummatkin voivat vuorotellen kirjoittaa?
Joudut muuten oikeasti miettimään sockettien käyttämisen uudelleen tehdessäsi event-pohjaista ohjelmaa Tkinterin kanssa. Createfilehandler olisi vissiin helpoin tapa, mutta ei ole tuettuna Windows-alustalla enää. Tcl socketit näyttäisi olevan yksi suosittu tapa. Säikeiden käytössä taas pitää olla tarkkana ja noudattaa tiettyjä sääntöjä.
Allaolevasta voisi olla apua, katso erityisesti: Time, Tools, Threads and Animation -kappale.
Uskon kyllä, että kun tuon saan tehtyä tekstipohjaisesti, niin kyllä se onnistuu myöhemmin Tkinterilläkin. =) Yksi asia kerrallaan...
Ok, kunhan varmistin, että tiedostat asian...
Tuosta tekstipohjaisesta versiosta: laita serverille ja asiakkaalle datan vastaanotto omiin säikeisiinsä. Tee lukitukset (lähetysvuorojen jako) serverin ja asiakkaan datan lähetysosassa.
Mitä nämä säikeet oikeen tarkoittavat? Miten ne tehdään? Koitin tehdä siten, että kun viestin on lähetetty, niin siirrytään seuraavaan osaan (vastaanottaminen), ja kun viesti tulee, niin sitten saa taas kirjoittaa. Koodin näetkin sivulla kaksi.
Tuli näköjään kiirus vielä työhommiin, joten en kerkiä nyt selittämään. Hae sanoilla Python sockets threads.
Tuolla oli jotain: http://ilab.cs.byu.edu/python/threadingmodule.
Moniajo tarkoittaa sitä, että tietokone tekee useaa asiaa "samaan aikaan" (käytännössä usein vuorotellen). Prosessi tarkoittaa yleensä yhtä ohjelmaa. Sama ohjelma voi toimia useassa säikeessä, jolloin se näyttää tekevän asioita samaan aikaan. Käyttötarkoituksia on monia, ja nyt moniytimisten prosessorien yleistyttyä kotikoneillakin saadaan esimerkiksi raskaat pelit toimimaan nopeammin, kun kaksi säiettä pystyy ihan oikeastikin toimimaan samaan aikaan.
Säikeiden käyttö ei ole mutkatonta. Joissain kielissä on tähän kyllä erittäin yksinkertaiset menetelmät (mm. Limbossa ja Javassa), mutta useimmissa vastuu on yhä ohjelmoijalla. Ongelmia tulee nimittäin siinä vaiheessa, kun kaksi säiettä yrittää käyttää samaa muuttujaa.
x = 1 A lukee muistista muuttujan x arvon (1) A korottaa luettua arvoa yhdellä (-> 2) B lukee muistista muuttujan x arvon (1) B korottaa luettua arvoa yhdellä (-> 2) B tallentaa muistiin arvon x = 2 A tallentaa muistiin arvon x = 2
Molemmat korottivat arvoa, mutta lopulta arvo kasvoikin vain yhdellä, koska korotukset tapahtuivat samaan aikaan.
Tällaisten tilanteiden takia data täytyy lukita. Kun toinen säie havaitsee, että tarvittava data on lukittu, se pysähtyy odottamaan, kunnes data vapautuu.
#!/usr/bin/python # -*- coding: UTF-8 -*- import threading lukko = threading.RLock() yhteinen_data = 1 def funktio(): # Lukitaan lukko ennen yhteisen datan käyttöä. lukko.acquire() yhteinen_data += 1 lukko.release() # Aloitetaan kaksi (samanlaista) säiettä. threading.Thread(target = funktio).start() threading.Thread(target = funktio).start()
Tämän pari päivää olen tässä pähkäillyt, mutta ei tuota tulosta. Höh!
Toinen vaihtoehto on tarkistaa data ei-blokkaavasta yhteydestä tietyin väliajoin.
#!/usr/bin/python # -*- coding: UTF-8 -*- import Tkinter import socket root = Tkinter.Tk() yhteys = socket.socket(socket.AF_INET, socket.SOCK_STREAM) yhteys.connect(("localhost", 1234)) # Käytetään lyhyttä timeouttia, jotta datan puuttuminen aiheuttaisi vain # poikkeuksen socket.timeout. Toinen vaihtoehto olisi yhteys.setblocking(False), # jolloin datan puuttumisesta tulisi socket.error kuten muistakin virheistä. yhteys.settimeout(0.001) def paasilmukka(): # Jatketaan "silmukkaa" tulevaisuudessakin. # Seuraava kierros 100 millisekunnin kuluttua. root.after(100, paasilmukka) if yhteys != None: lue_data() def lue_data(): global yhteys try: data = yhteys.recv(1024) except socket.timeout: return if len(data) == 0: yhteys = None print "Yhteys katkaistu!" return print("Saatiin '%s'" % data) # Aloitetaan "silmukka". root.after(100, paasilmukka) root.mainloop()
Ongelmana oikeastaan on se, että miten saan clientin/serverin vuorotellen odottamaan dataa ja muutoin lähettämään dataa, ja vielä samassa ohjelmassa (Ensin valitaan toimitaanko serverinä vai clienttinä)!
Piirrä pelin toiminnasta kaavio (hieman tähän tapaan), niin asia ehkä selkenee.
def peli_alkaa_palvelimella(): global oma, vuorossa vuorossa = "X" oma = "X" vastustaja = "XO".replace(oma, "") yhteys.send(vastustaja) yhteys.send(vuorossa) def peli_alkaa_asiakkaalla(): global oma, vuorossa # Tämä data on saatava heti, timeout pois. yhteys.settimeout(None) oma = yhteys.recv(1) vuorossa = yhteys.recv(1) yhteys.settimeout(0.001) def paasilmukka(): if peli_kaynnissa: if vuorossa <> oma: # Vastustajan vuoro, katsotaan, onko siirto tullut. yrita_vastaanottaa_siirto() def klikattu(x, y): if vuorossa <> oma: # Ei ole oma vuoro, ei käsitellä klikkausta. return ilmoita_siirto_ja_vastaanota_ok()
Aihe on jo aika vanha, joten et voi enää vastata siihen.