Topic: pygame e snake (tanto per cambiare)  (Letto 2234 volte)

0 Utenti e 1 Visitatore stanno visualizzando questo topic.

Offline Trizio

  • python unicellularis
  • *
  • Post: 40
  • Punti reputazione: 1
    • Mostra profilo
pygame e snake (tanto per cambiare)
« il: Ottobre 15, 2017, 14:22 »
In questi giorni ho cercato di applicare le nozioni fin qui apprese, creando il solito clone di snake. Per fare questo ho dato un'occhiata a vari programmi scritti da altri, cercando di capire la logica da implementare e soprattutto le varie funzioni di pygame che mi servivano. Ne è uscito qualcosa di interessante, non particolarmente originale, ma con qualche illuminazione su cui metto il mio copyright (joke).

Il gioco doveva seguire una particolare sequenza di azioni:
  • il serpente appare e scompare per tre secondi per far capire al giocatore che il gioco sta per cominciare;
  • main loop del gioco;
  • game over, con il serpente e la mela che diventano bianchi per tre secondi per far capire al giocatore che il gioco è finito.
Dopodiché il tutto si ripete fino a quando il giocatore non decide di chiudere il gioco premendo ESC o chiudendo la finestra normalmente. Niente menu e niente punteggio. Solo gameplay.

Ora, ho cercato di dividere la logica del programma in vari moduli e funzioni, così come si dovrebbe fare. Quello che vi chiedo non è di leggere TUTTO il programma (ci mancherebbe), bensì di darci una rapida occhiata per stabilire se è scritto bene o se invece è il caso di fare alcune modifiche (es. alcune funzioni possono  essere fattorizzate, ecc. ecc.)... in breve, se il programma è leggibile.

Se volete provare il gioco, potete semplicemente copiare i seguenti moduli e incollarli nella stessa directory. Per muovere il serpente bisogna usare i tasti WASD, come negli FPS.
Non ci sono file esterni. Inoltre, il tutto dovrebbe essere esente da bug -  fino a prova contraria - ho lavorato ai vari casi limite.

cnst.py
Questo modulo contiene le costanti utilizzate dal gioco. L'idea di piazzarle in un modulo a sé stante l'ho copiata da un platform scritto con pygame. Una buona idea, devo dire, rende il tutto molto più leggibile.

import pygame

WINDOWWIDTH = 320
WINDOWHEIGHT = 240
DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))

CELLSIZE = 10
CELLWIDTH = WINDOWWIDTH / CELLSIZE
CELLHEIGHT = WINDOWHEIGHT / CELLSIZE
ALL_COORDS = []
for x in range(0, CELLWIDTH):
    for y in range(0, CELLHEIGHT):
        ALL_COORDS.append((x, y))

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
RED   = (255, 0, 0)

FPS = 15
CLOCK = pygame.time.Clock()



snake.py

def get_new_head(direction, old_head_coordinates):
    """Given a string which specify the snake's direction and a tuple
    containing the old head coordinates, return a tuple containing the
    new head coordinates.
    """
    x, y = old_head_coordinates
    if direction == 'up':
        return (x, y-1)
    elif direction == 'down':
        return (x, y+1)
    elif direction == 'right':
        return (x+1, y)
    elif direction == 'left':
        return (x-1, y)



apple.py

import random
from cnst import ALL_COORDS


def get_random_coords(snake_coords):
    """Given a list containing all the coordinates occupied by the snake,
    return a tuple of free coordinates.
    """
    all_free = [coords for coords in ALL_COORDS if coords not in snake_coords]
    return random.choice(all_free)



game.py
Il modulo principale.

import sys, pygame, snake, apple
from cnst import *
from pygame.locals import *


def get_cell_rect(coords):
    """Given a tuple containing a pair of coordinates, return a rect
    object.
    """
    x, y = coords
    return pygame.Rect(x*CELLSIZE+1, y*CELLSIZE+1, CELLSIZE-1, CELLSIZE-1)


def draw_cell_square(rect, color):
    """Given a rect object and a color, draw a square on the rect."""
    pygame.draw.rect(DISPLAYSURF, color, rect)


def initialize_game():
    # initialize snake's starting coordinates and direction
    snake_coords = [(5, 11), (4, 11), (3, 11)]
    snake_direction = 'right'
   
    # initialize first apple coordinates
    apple_coords = apple.get_random_coords(snake_coords)

    # create the needed rects
    snake_rects = [get_cell_rect(coords) for coords in snake_coords]

    # blink the snake for three seconds while checking if player wants to quit
    for half_sec in range(1, 7):
        for event in pygame.event.get():
            if event.type == QUIT:
                terminate()
            elif event.type == KEYDOWN:
                if event.key == K_ESCAPE:
                    terminate()
        else:
            for rect in snake_rects:
                if half_sec % 2 == 0:
                    draw_cell_square(rect, GREEN)
                else:
                    DISPLAYSURF.fill(BLACK)
            pygame.display.update(snake_rects)
            pygame.time.delay(500)
    return snake_coords, snake_direction, apple_coords


def run_game(snake_coords, snake_direction, apple_coords):
    while True:
        # manage pygame event queue
        event = pygame.event.poll()
        if event.type == QUIT:
            terminate()
        elif event.type == KEYDOWN:
            if event.key == K_a and snake_direction != 'right':
                snake_direction = 'left'
            elif event.key == K_d and snake_direction != 'left':
                snake_direction = 'right'
            elif event.key == K_w and snake_direction != 'down':
                snake_direction = 'up'
            elif event.key == K_s and snake_direction != 'up':                 
                snake_direction = 'down'
            elif event.key == K_ESCAPE:
                terminate()

        # add snake's new head inside its data structure
        new_head_coords = snake.get_new_head(snake_direction, snake_coords[0])
        snake_coords.insert(0, new_head_coords)

        # check if snake's new head has hit an edge
        x, y = new_head_coords
        if x == -1 or x == CELLWIDTH:
            return snake_coords, apple_coords
        if y == -1 or y == CELLHEIGHT:
            return snake_coords, apple_coords
     
        # check if snake's new head has hit its body
        for body_coords in snake_coords[1:]:
            if new_head_coords == body_coords:
                return snake_coords, apple_coords

        # check if snake's new head has eaten the apple
        if new_head_coords == apple_coords:
            apple_coords = apple.get_random_coords(snake_coords)
            tail_coords = snake_coords[-1]  # previous iteration tail
        else:
            tail_coords = snake_coords.pop()
       
        # create the needed rects
        head_rect = get_cell_rect(new_head_coords)
        tail_rect = get_cell_rect(tail_coords)
        apple_rect = get_cell_rect(apple_coords)

        # draw background, snake's head and apple
        DISPLAYSURF.fill(BLACK)
        draw_cell_square(head_rect, GREEN)
        draw_cell_square(apple_rect, RED)
       
        # update selected display rects
        rects = [head_rect, tail_rect, apple_rect]
        pygame.display.update(rects)
        CLOCK.tick_busy_loop(FPS)


def game_over(snake_coords, apple_coords):
    # create the needed rects
    snake_rects = [get_cell_rect(coords) for coords in snake_coords]
    apple_rect = get_cell_rect(apple_coords)

    # draw snake's body and apple
    for rect in snake_rects:
        draw_cell_square(rect, WHITE)
    draw_cell_square(apple_rect, WHITE)
   
    # update selected display rects
    rects = snake_rects[:]
    rects.append(apple_rect)
    pygame.display.update(rects)
   
    # freeze the game for three seconds but check if player wants to quit
    for half_sec in range(1, 7):
        for event in pygame.event.get():
            if event.type == QUIT:
                terminate()
            elif event.type == KEYDOWN:
                if event.key == K_ESCAPE:
                    terminate()
        else:
            pygame.time.delay(500)
   
    # clear the screen for next iteration
    DISPLAYSURF.fill(BLACK)
    pygame.display.update(rects)
    return


def terminate():
    pygame.quit()
    sys.exit()


def main():
    pygame.init()
    pygame.display.set_caption('Snake')

    # block unwanted events
    blocked_events = [MOUSEMOTION, MOUSEBUTTONUP, MOUSEBUTTONDOWN]
    pygame.event.set_blocked(blocked_events)

    while True:
        init_args = initialize_game()
        final_args = run_game(*init_args)
        game_over(*final_args)


if __name__ == '__main__':
    main()


L'"idea rivoluzionaria" di cui vi parlavo consiste nel fatto che ad ogni frame disegno solo la testa e la mela. Il corpo del serpente è solo un'"eco" della testa dei precedenti frame. Questo dovrebbe essere molto più efficiente e non l'ho visto fare da nessun altro. Già, niente di trascendentale, ma mi sembrava un'idea eccellente.

Comunque, le prime due funzioni in quest'ultimo modulo sono delle... ehm... utility. Probabilmente andrebbero scritte in un modulo a parte, ma non sapevo come chiamarlo. Servono rispettivamente a creare un oggetto rect e a disegnare un quadrato sullo stesso. La mela e il corpo del serpente sono uguali, cambia solo il colore, per cui le due funzioni servono per entrambi.

Per il resto, ho diviso le tre sequenze di gioco in tre funzioni distinte che si "passano" gli argomenti di cui hanno bisogno.

Che ne pensate? Qualcosa da migliorare? Il programma è scritto come farebbe un... ehm, professionista? È leggibile?

Ah, già, quasi me ne dimenticavo. Nelle funzioni initialize_game e game_over ho trovato un espediente per fermare l'immagine per tre secondi. Probabilmente non è granché, e di sicuro c'è un modo migliore, ma non ho trovato niente di meglio cercando (rapidamente) nella libreria di pygame. Praticamente metto il pausa il gioco ogni mezzo secondo e controllo che l'utente non abbia voglia di chiudere il gioco.
« Ultima modifica: Ottobre 16, 2017, 11:01 da Trizio »

Offline riko

  • python deus
  • *
  • moderatore
  • Post: 7.453
  • Punti reputazione: 12
    • Mostra profilo
    • RiK0 Tech Temple
Re:pygame e snake (tanto per cambiare)
« Risposta #1 il: Ottobre 16, 2017, 18:00 »
Guarda, come al solito, complimenti. Si vede che poni molta cura in quello che fai, e non lo vedo spesso nel codice dei "principianti".

Varie cosine, tanto per fare due diligence. Ovviamente usero' uno stile tagliente e sarcastico per controbilanciare la stucchevolezza dell'apertura del post (ho una reputazione di burbero da difendere).

* cnst.py? se hai dei tasti rotti e non puoi permetterti una tastiera nuova, dammi il tuo indirizzo e te la faccio mandare. :) se no chiamalo constants.py e basta... tanto quello e'. e si... e' una pratica comune anche in codice "serio", per un progetto di queste dimensioni e' una buona idea. poi si potrebbe aprire una discussione lunghissima sul perfetto piazzamento delle costanti, ma tipicamente il beneficio di usare costanti e avere uno schema chiaro di dove vanno porta piu' beneficio che passare da uno schema decoroso a quello perfetto. Quindi...
* Ah... in un mondo di costanti, dubita di quello che non e' costante. ALL_COORDS e' meglio che sia una tupla, non una lista. Considera anche di avere semplicemente un generatore per quelle costanti... tanto mi aspetto che ti serva piu' che altro per iterarci sopra. E allora il generatore e' la cosa giusta. Ovviamente quindi non una costante, ma una funzione che ti torna il generatore.
* CLOCK mi inquieta. E' un oggetto "ricco" messo come costante. Ora io capisco che sia molto comodo averlo in un posto comodo (appunto), ma... appena ti metti a scrivere i test diventa una scomodita'. Io ti suggerirei di metterti li e aggiungere il famoso clock alle funzioni che ne hanno bisogno come parametro. E poi magari ti rendi conto che qualche gruppo di funzioni che operano su un set di dati comuni finisce che dovrebbero essere una classe (e allora il clock diventa un bell'attributo della classe e le cose tornano snelle meglio di prima).
* up, down, etc... costanti.
* def get_cell_rect(coords): -> def get_cell_rect((x, y)), funzionalmente equivalente a quello che hai scritto, con una riga di codice in meno. Poi verrebbe da chiedersi se le coordinate visto che le usi spesso non debbano essere un oggetto (tipo una namedtuple). A me verrebbe da dire di si.
* non usare * import.
* initialize_game contiene troppa logica. e molte delle cose che ha direttamente in pancia dovrebbero essere funzioni. inoltre fa *due* cose: inizializza il sistema dal punto di vista del software, *poi* passa il controllo all'utente per fare partire il gioco (dal punto di vista dell'utente). Questo e' gia' praticamente un "main" (o meglio, parte di).  Grosso modo la stessa cosa vale per le funzioni nello stesso modulo. E vedrai anche che ci sono pezzi comuni che puoi astrarre.

Offline Trizio

  • python unicellularis
  • *
  • Post: 40
  • Punti reputazione: 1
    • Mostra profilo
Re:pygame e snake (tanto per cambiare)
« Risposta #2 il: Ottobre 19, 2017, 15:54 »
* cnst.py? se hai dei tasti rotti e non puoi permetterti una tastiera nuova, dammi il tuo indirizzo e te la faccio mandare. :) se no chiamalo constants.py e basta... tanto quello e'. e si... e' una pratica comune anche in codice "serio", per un progetto di queste dimensioni e' una buona idea. poi si potrebbe aprire una discussione lunghissima sul perfetto piazzamento delle costanti, ma tipicamente il beneficio di usare costanti e avere uno schema chiaro di dove vanno porta piu' beneficio che passare da uno schema decoroso a quello perfetto. Quindi...

L'avevo chiamato cnst.py perché così lo avevano chiamato nel platform da cui ho preso l'idea. Ma effettivamente è meglio essere chiari.


* Ah... in un mondo di costanti, dubita di quello che non e' costante. ALL_COORDS e' meglio che sia una tupla, non una lista. Considera anche di avere semplicemente un generatore per quelle costanti... tanto mi aspetto che ti serva piu' che altro per iterarci sopra. E allora il generatore e' la cosa giusta. Ovviamente quindi non una costante, ma una funzione che ti torna il generatore.

Già, questo è curioso. In effetti, ho cercato di capire quale fosse il "contenitore" migliore per le coordinate e ho scelto le tuple per via della loro immutabilità. Poi qui ho finito per inserire una sequenza costante di tuple in una lista...

Comunque sia, sì, ALL_COORDS mi serve unicamente per iterarci sopra quando ho bisogno di trovare delle nuove coordinate per la mela. Il generatore è un'ottima idea, ma è un concetto che non ho ancora studiato. Ho dato un'occhiata al mio manuale e, se ho capito bene, allora dovrei fare una funzione che torna il generatore:

def gen_all_coords():
    for x in range(0, CELLWIDTH):
        for y in range(0, CELLHEIGHT):
            yield (x, y)


Dopodiché la chiamo all'interno della funzione in cui mi serve:

def get_random_coords(snake_coords):
    """Given a list containing all the coordinates occupied by the snake,
    return a random tuple of free coordinates.
    """
    all_coords = gen_all_coords()
    free_coords = [xy for xy in all_coords if xy not in snake_coords]
    return random.choice(free_coords)



* CLOCK mi inquieta. E' un oggetto "ricco" messo come costante. Ora io capisco che sia molto comodo averlo in un posto comodo (appunto), ma... appena ti metti a scrivere i test diventa una scomodita'. Io ti suggerirei di metterti li e aggiungere il famoso clock alle funzioni che ne hanno bisogno come parametro. E poi magari ti rendi conto che qualche gruppo di funzioni che operano su un set di dati comuni finisce che dovrebbero essere una classe (e allora il clock diventa un bell'attributo della classe e le cose tornano snelle meglio di prima).

Questo me lo segno, perché devo ancora studiare le classi.


* up, down, etc... costanti.

Vero. Suppongo che anche blocked_events alla linea 148 del modulo game.py posso inserirlo tra le costanti (ovviamente come una tupla). Si tratta di eventi che devono essere sempre ignorati.


* def get_cell_rect(coords): -> def get_cell_rect((x, y)), funzionalmente equivalente a quello che hai scritto, con una riga di codice in meno. Poi verrebbe da chiedersi se le coordinate visto che le usi spesso non debbano essere un oggetto (tipo una namedtuple). A me verrebbe da dire di si.

Mmm, sì, a ben vedere le named tuple sono il container ideale in questo contesto. Però sul mio manuale, il Lutz, si dice che quando si usano le named tuple si deve accettare un compromesso: in cambio di una maggiore versatilità si incorre in una penalità nelle performance. Quello che non specifica è l'entità di questa penalità...

Mmm, d'altro canto, qui non ho un problema di prestazioni e l'idea di usare le named tuple mi piace molto.

*UPDATE* beh, ho modificato il codice includendo tutti i suggerimenti, comprese le named tuple. Questo calo nelle prestazioni, se c'è, è del tutto trascurabile qui. In compenso il codice è molto più leggibile.


 
* non usare * import.

Da qualche parte su Stack Overflow ho letto che utilizzare import * è controindicato perché, vado a memoria, "si importa un sacco di robaccia inutile". Presumo quindi che la ragione di questo abbia qualcosa a che fare con la memoria. La parte del manuale che tratta dei moduli non l'ho ancora studiata (ehm, sì, mi sto rendendo conto del fatto che sarebbe il caso di continuare il mio studio), mi sono limitato a farmi una vaga idea leggendo il codice altrui.

Però, poniamo il caso che io debba importare la costante RED dal modulo constants.py. Allora i casi sono due:

import constants

constants.RED


Oppure:

from constants import RED

RED


Quale usare quando devo importare un nome?

Leggendo il codice altrui ho trovato anche cose del tipo:

import pygame

pg = pygame


È una buona abitudine? Perché non mi sembra che agevoli particolarmente la leggibilita... anzi, io direi che la compromette.


* initialize_game contiene troppa logica. e molte delle cose che ha direttamente in pancia dovrebbero essere funzioni. inoltre fa *due* cose: inizializza il sistema dal punto di vista del software, *poi* passa il controllo all'utente per fare partire il gioco (dal punto di vista dell'utente). Questo e' gia' praticamente un "main" (o meglio, parte di).  Grosso modo la stessa cosa vale per le funzioni nello stesso modulo. E vedrai anche che ci sono pezzi comuni che puoi astrarre.

Immaginavo. Devo dire che mi lascia un po' perplesso questa cosa della fattorizzazione. Voglio dire, ne capisco la logica e l'utilità, ma alle volte, quando leggo il codice scritto da altri, mi sento un po' come il cliente delle poste che viene mandato da uno sportello all'altro. Inizio col leggere la funzione main, da qui mi si rimanda ad una funzione in un altro modulo, poi da qui magari vengo mandato su una funzione che sta in un altro modulo ancora... è un po' frustrante. In alcuni casi, quando c'e' del codice duplicato, non c'è alternativa migliore. Altre volte...

Per esempio, prendiamo la funzione run_game del modulo game.py e nella fattispecie queste righe:

        # check if snake's new head has hit an edge
        x, y = new_head_coords
        if x == -1 or x == CELLWIDTH:
            return snake_coords, apple_coords
        if y == -1 or y == CELLHEIGHT:
            return snake_coords, apple_coords
     
        # check if snake's new head has hit its body
        for body_coords in snake_coords[1:]:
            if new_head_coords == body_coords:
                return snake_coords, apple_coords


Ora, questo codice lo uso una volta sola, qui. Potrei creare due funzioni distinte, che piazzerei nel modulo snake.py:

def edge_hit((x, y)):
    """Given the head coords, return True if the head hits the edge of
    the screen, otherwise return False.
    """
    if x == -1 or x == CELLWIDTH or y == -1 or y == CELLHEIGHT:
        return True
    else:
        return False


def body_hit(snake_coords):
    """Given a list containing the snake's coords, return True if the
    head of the snake hits its body, otherwise return False.
    """
    for body_coords in snake_coords[1:]:
        if snake_coords[0] == body_coords:
            return True
    else:
        return False


Sicché, tornando alla funzione run_game:

        # check game over conditions
        if snake.edge_hit(snake_coords[0]) or snake.body_hit(snake_coords):
            return snake_coords, apple_coords


Tralasciando il fatto che si fa così perche si fa così (e ora che l'ho messa in questi termini spero che si faccia davvero così), siamo sicuri che ne valga davvero la pena?
« Ultima modifica: Ottobre 20, 2017, 16:47 da Trizio »

Offline riko

  • python deus
  • *
  • moderatore
  • Post: 7.453
  • Punti reputazione: 12
    • Mostra profilo
    • RiK0 Tech Temple
Re:pygame e snake (tanto per cambiare)
« Risposta #3 il: Ottobre 21, 2017, 11:02 »
def gen_all_coords():
    for x in range(0, CELLWIDTH):
        for y in range(0, CELLHEIGHT):
            yield (x, y)


...

Si, corretto. Le parentesi attorno ad x e y non sono necessarie, ma al di la di quello, tutto bene.

Detto questo, parliamo di astrazione... vista come e' fatta quella funzione, io andrei di

def gen_all_coords(max_width, max_height)

o al limite

def gen_all_coords(max_width=CELLWIDTH, max_height=CELLWEIGHT)

Mi sembra ragionevole avere una funzione che lavora indifferentemente a prescindere dall'attuale area di gioco.

> Questo me lo segno, perché devo ancora studiare le classi.

Scusami... assumo quindi che non hai ancora finito la prima lettura di un manuale.
Beh, guardatele pure. Tutto sommato e' un concetto piu' facile dei generatori.


* up, down, etc... costanti.

> Vero. Suppongo che anche blocked_events alla linea 148 del modulo game.py posso inserirlo tra le costanti (ovviamente come una tupla). Si tratta di eventi che devono essere sempre ignorati.

Si. Insisto, finche' il progetto e' piccolo. Poi bisognerebbe capire meglio come fare a far sparire la maggior parte delle costanti e metterle solo a chi deve usarle.

> Mmm, sì, a ben vedere le named tuple sono il container ideale in questo contesto. Però sul mio manuale, il Lutz, si dice che quando si usano le named tuple si deve accettare un compromesso: in cambio di una maggiore versatilità si incorre in una penalità nelle performance. Quello che non specifica è l'entità di questa penalità...

Ora... se il Lutz evitasse questi commenti sarebbe meglio. Resta un ottimo libro, ma secondo me scrivere cose del genere porta fuori strada. Perche' si, formalmente ha ragione. Ma e' un tipo di commento che scoraggia un principiante per nessun motivo. Passera' parecchio tempo prima che tu debba porti il problema che il tuo programma e' troppo lento *perche' usa le namedtuples*. E se succede il fix e' semplice: usa gli indici numerici dove e' davvero critico. Il motivo e' semplicemente che per fare foo.bar lui va a cercare la chiave bar che gli da l'indice della tupla dove sta quell'attributo, e poi accede all'elemento. Se invece hai una tupla normale (o usi indici numerici con le tuple) vai diretto all'elemento. E se pure ti piacesse continuare a usare l'accesso per attributo ti puoi fare una classe che fa questa roba in modo efficiente. O quello che ti pare. Ma insisto, non e' un problema. Sinceramente per me non e' mai stato un problema, che e' dire qualcosa. Semmai salta fuori che e' proprio Python il problema.

Insomma, fai bene a non distrarti completamente sulle performance, ma specie in questa fase avrai piu' benefici scrivendo le cose in modo sensato, usando algoritmi e strutture dati giuste e cosi' via. Le micro-ottimizzazioni lasciale per quando avrai misurato un problema di performance (ovvero e' oggettivamente: non e' che fai misure e dici "potrei essere piu' veloce"; semplicemente ad un certo punto il programma non fa quello che deve nel tempo necessario). In molti casi questo non succede mai.

> Mmm, d'altro canto, qui non ho un problema di prestazioni e l'idea di usare le named tuple mi piace molto.

Appunto.

Citazione
Da qualche parte su Stack Overflow ho letto che utilizzare import * è controindicato perché, vado a memoria, "si importa un sacco di robaccia inutile". Presumo quindi che la ragione di questo abbia qualcosa a che fare con la memoria. La parte del manuale che tratta dei moduli non l'ho ancora studiata (ehm, sì, mi sto rendendo conto del fatto che sarebbe il caso di continuare il mio studio), mi sono limitato a farmi una vaga idea leggendo il codice altrui.

Allora, il problema non e' in generale la memoria. Quando hai importato il modulo la maggior parte della memoria che doveva andare e' andata comunque. La variante * semplicemente prenderebbe un pochetto di piu' perche' ci sarebbero un numero di chiavi in piu' in qualche dizionario. Mi verrebbe da dire che e' eccezionale che questa cosa diventi davvero un problema. Mi aspetto che possa succedere solo in presenza di qualcosa di patologico.

Il problema, come quasi sempre in Python, e' di pura pulizia.

1. se tieni le cose qualificate, e' piu' semplice capire cosa sono.
2. e' piu' facile evitare conflitti nei nomi (i conflitti sui nomi dei moduli sono strettamente piu' rari dei conflitti sugli oggetti contenuti nei moduli) e risolverli e' estremamente piu' semplice.

In generale il problema e' che quando fai uno star import, non hai idea di cosa stai importando. Non solo: modifiche innocue alle tue dipendenze possono romperti.

Comunque il problema e' ben discusso qui. https://www.python.org/dev/peps/pep-0008/#imports
In generale vale la pena conoscere *bene* cosa c'e' in quel documento. Tu mi sembra sia molto appassionato di scrivere codice "pulito" e "secondo best practice". Bene... amerai quel documento.

Però, poniamo il caso che io debba importare la costante RED dal modulo constants.py. Allora i casi sono due:

Citazione
import constants

constants.RED


Oppure:

from constants import RED

RED


Quale usare quando devo importare un nome?

In quasi tutte le circostanze la prima delle due. Con possibilmente variazioni tipo:

from module import submodule

(e poi submodule.RED)

oppure

from myproject import constants
from bombastic import constants as bombastic_constants

Ma in generale cerco sempre di tenere la qualificazione a livello di modulo/sottomodulo

Aggiungo... cosa succede se per la tua libreria RED e' una stringa (che so... hai tre colori, 'red', 'black', 'blue'... ha senso usare costanti di questo tipo), mentre per una qualche libreria che usi RED e' una tupla di 3 byte (tipo rappresentazione RGB). Ad un certo punto qualche pezzo di codice usera' il RED sbagliato e un intern dovra' essere sacrificato sull'altare della type safety per riportare ordine nel cosmo.

Oh, poi ci sono tutta una serie di trucchetti che nessuno insegna... tipo os ha un modulo path, che contiene roba utile. Sfortunatamente path e' anche un nome comune per chiamare una variabile di tipo path. E non sempre ci sono nomi altrettanto efficaci (e' solo un path, non ha nulla che la differenzia dagli altri path).

Beh, per questo spesso faccio solo import os e poi os.path.dirname o quello che e'. In questo modo posso usare path liberamente, perche' il modulo path rimane os.path.
Alternativamente potrei fare from os import path as os_path, ma non mi sembra che porti vantaggi rilevanti, quindi faccio solo os. E nota: tutto questo e' solo fatto in nome della leggibilita' e della scelta dei nomi. Non ci sono considerazioni di performance o altro.

> Leggendo il codice altrui ho trovato anche cose del tipo:

Ottima scelta leggere il codice altrui. Poi suggerisco... leggi il codice, quello buono. Ora ci vuole un po' di esperienza per navigare, ma in generale piu' un pezzo di codice e' scritto da sviluppatori veri, piu' la qualita' tende ad essere alta (tutta gente con background forte di software engineering). Insomma... salvo che anche nella stdlib ci sono cose che fanno vomitare, in generale la qualita' del codice e' buona. Similarmente per librerie ad alto profilo.

Viceversa troverai che robe tipo numpy devono fare una serie di concessioni per motivi di performance. La roba di data science e' in generale scritta da persone con un diverso background, hanno scelte di API spesso discutibili.

Vai a pescare una libreria scritta da un hobbista per parlare con il suo simil-arduino... beh, se ti serve usala. Studiane il codice per capire cosa fa. Ma non prendere esempio dallo stile. E cosi' via.

Pygame a me verrebbe da dire che e' una roba che deve fare un sacco di concessioni alla pragmatica. Fra performance in Python, un modello che viene piu' dalla scrittura di giochi "alla vecchia maniera" che dal mondo del software engineering modello. Cioe'... la PyGame alla grande! Divertiti. Non ha nulla che non va.

Ma diciamo che una serie di cose che potresti vedere molto comunemente in quel contesto potrebbe non essere apprezzata tantissimo dalla wider python community.

Citazione
import pygame

pg = pygame


È una buona abitudine? Perché non mi sembra che agevoli particolarmente la leggibilita... anzi, io direi che la compromette.

Sono d'accordo con te. E' una cacata pazzesca.



Denota sonora ignoranza di Python. Se il problema e' dare un nome diverso ad un modulo (per qualunque ragione), fai come dico io.
Si fa, specie per la gente che come me rifiuta gli star import.

Per dire... quando uso numpy o scipy o pandas o sta roba qui, mi trovo con librerie che hanno nel loro namespace globale un quintale di roba. E in generale nei posti in cui li uso, uso quasi solo roba che viene dalla libreria. Quindi, visto che il contesto e' decisamente chiaro quello che succede e' tipo

import numpy as np
import pandas as pd

Perche' la qualitica e' chiara (continuo a sapere chi viene da cosa), ma il codice rimane un piu' coinciso da leggere. Nota, una cosa del genere la faccio solo con librerie molto riconoscibile. Cioe' grosso modo chiunque vede una pagina piena di np (specie guardando i nomi delle funzioni) capisce che stiamo parlando di numpy. Eviterei di abusare di questa feature in caso di librerie poco note. Che se mi trovo un

prd.do_something()

non sono proprio contento.

* initialize_game contiene troppa logica. e molte delle cose che ha direttamente in pancia dovrebbero essere funzioni. inoltre fa *due* cose: inizializza il sistema dal punto di vista del software, *poi* passa il controllo all'utente per fare partire il gioco (dal punto di vista dell'utente). Questo e' gia' praticamente un "main" (o meglio, parte di).  Grosso modo la stessa cosa vale per le funzioni nello stesso modulo. E vedrai anche che ci sono pezzi comuni che puoi astrarre.

Immaginavo. Devo dire che mi lascia un po' perplesso questa cosa della fattorizzazione. Voglio dire, ne capisco la logica e l'utilità, ma alle volte, quando leggo il codice scritto da altri, mi sento un po' come il cliente delle poste che viene mandato da uno sportello all'altro. Inizio col leggere la funzione main, da qui mi si rimanda ad una funzione in un altro modulo, poi da qui magari vengo mandato su una funzione che sta in un altro modulo ancora... è un po' frustrante. In alcuni casi, quando c'e' del codice duplicato, non c'è alternativa migliore. Altre volte...

Per esempio, prendiamo la funzione run_game del modulo game.py e nella fattispecie queste righe:

        # check if snake's new head has hit an edge
        x, y = new_head_coords
        if x == -1 or x == CELLWIDTH:
            return snake_coords, apple_coords
        if y == -1 or y == CELLHEIGHT:
            return snake_coords, apple_coords
     
        # check if snake's new head has hit its body
        for body_coords in snake_coords[1:]:
            if new_head_coords == body_coords:
                return snake_coords, apple_coords


Ora, questo codice lo uso una volta sola, qui. Potrei creare due funzioni distinte, che piazzerei nel modulo snake.py:

def edge_hit((x, y)):
    """Given the head coords, return True if the head hits the edge of
    the screen, otherwise return False.
    """
    if x == -1 or x == CELLWIDTH or y == -1 or y == CELLHEIGHT:
        return True
    else:
        return False


def body_hit(snake_coords):
    """Given a list containing the snake's coords, return True if the
    head of the snake hits its body, otherwise return False.
    """
    for body_coords in snake_coords[1:]:
        if snake_coords[0] == body_coords:
            return True
    else:
        return False


Sicché, tornando alla funzione run_game:

        # check game over conditions
        if snake.edge_hit(snake_coords[0]) or snake.body_hit(snake_coords):
            return snake_coords, apple_coords


Citazione
Tralasciando il fatto che si fa così perche si fa così (e ora che l'ho messa in questi termini spero che si faccia davvero così), siamo sicuri che ne valga davvero la pena?

Se scrivessi i test per il tuo codice, non ti porresti questa domanda. Nel senso che testare due funzioni corte e' molto piu' semplice che testarne una lunga.

Detto questo... hai due funzioni per testare se hai sbattuto la testa sul muro o ti sei troncato il corpo. Mi sembra utile.
Vuoi vedere un side effect? Io il tuo codice lo avevo gia' visto... ma non mi ero accorto di un errore che fai. L'errore e' che scrivi una roba in molto complicato quando potresti semplificare. Solo che fintanto che era un coso unico, lo avevo solo preso per assunto. Quando mi e' stato chiaro che in effetti quel pezzo di codice deve solo dirmi se la testa coincide con il corpo, mi e' stato chiaro che hai molti modi piu' efficienti e/o coincisi per risolvere questo problema:

a. snake[0] in snake[1:] /
b. Counter(snake) e poi guardi se qualcosa ha 2 occorrenze e in generale ogni sistema buono per
c. snake.count(snake[0]) > 1
d. etc etc etc

Tutti questi sistemi sono molto coincisi. Ma ancora una volta non spiegano *esattamente* cosa vuoi fare. Non ti dicono perche' snake.count(snake[0]) > 1... ovvero non ti dicono come risolve il tuo problema.

Mettilo dentro un metodo body_hit ed e' immediatamente chiaro *perhe'* hai scritto quella cosa e come mai quella cosa funziona.
« Ultima modifica: Ottobre 21, 2017, 11:55 da riko »

Offline Trizio

  • python unicellularis
  • *
  • Post: 40
  • Punti reputazione: 1
    • Mostra profilo
Re:pygame e snake (tanto per cambiare)
« Risposta #4 il: Ottobre 24, 2017, 17:18 »
Si, corretto. Le parentesi attorno ad x e y non sono necessarie, ma al di la di quello, tutto bene.

Detto questo, parliamo di astrazione... vista come e' fatta quella funzione, io andrei di

def gen_all_coords(max_width, max_height)

o al limite

def gen_all_coords(max_width=CELLWIDTH, max_height=CELLWEIGHT)

Mi sembra ragionevole avere una funzione che lavora indifferentemente a prescindere dall'attuale area di gioco.

Mmm, immagino che sia utile per testare la funzione e facilitare eventuali modifiche.


In generale il problema e' che quando fai uno star import, non hai idea di cosa stai importando. Non solo: modifiche innocue alle tue dipendenze possono romperti.

Comunque il problema e' ben discusso qui. https://www.python.org/dev/peps/pep-0008/#imports
In generale vale la pena conoscere *bene* cosa c'e' in quel documento. Tu mi sembra sia molto appassionato di scrivere codice "pulito" e "secondo best practice". Bene... amerai quel documento.

Sì, anche il Lutz rimanda a questa pagina. La sto studiando parallelamente al manuale, ma non avendo studiato i moduli, questa parte non l'avevo ancora letta.


Se scrivessi i test per il tuo codice, non ti porresti questa domanda. Nel senso che testare due funzioni corte e' molto piu' semplice che testarne una lunga.

Gli unit test? Eh, ne ho sentito parlare. Il problema è che il manuale comincia a parlarne verso la fine.

Detto questo... hai due funzioni per testare se hai sbattuto la testa sul muro o ti sei troncato il corpo. Mi sembra utile.
Vuoi vedere un side effect? Io il tuo codice lo avevo gia' visto... ma non mi ero accorto di un errore che fai. L'errore e' che scrivi una roba in molto complicato quando potresti semplificare. Solo che fintanto che era un coso unico, lo avevo solo preso per assunto. Quando mi e' stato chiaro che in effetti quel pezzo di codice deve solo dirmi se la testa coincide con il corpo, mi e' stato chiaro che hai molti modi piu' efficienti e/o coincisi per risolvere questo problema:

a. snake[0] in snake[1:] /
b. Counter(snake) e poi guardi se qualcosa ha 2 occorrenze e in generale ogni sistema buono per
c. snake.count(snake[0]) > 1
d. etc etc etc

Tutti questi sistemi sono molto coincisi. Ma ancora una volta non spiegano *esattamente* cosa vuoi fare. Non ti dicono perche' snake.count(snake[0]) > 1... ovvero non ti dicono come risolve il tuo problema.

Mettilo dentro un metodo body_hit ed e' immediatamente chiaro *perhe'* hai scritto quella cosa e come mai quella cosa funziona.

Shoot! Qui non ho proprio scusanti: dovevo accorgermene.  :embarrassed:
Mi hai convinto.



Tornerò a lavorare su questo progettino quando avrò studiato le classi. Nel frattempo, ti ringrazio, Riko, per la tua disponibilità.