Automi a stati finiti (ASF o FSM)

Python Automi Videogiochi Simulazione Stati Comportamenti
Liceo, Scientifico S.A., TT informatica

Contenuto: introduzione agli automi a stati finiti come modelli matematici fondamentali per rappresentare comportamenti sequenziali, validare input e modellare sistemi reattivi, mostrando la loro utilità sia nella didattica che nella progettazione di software e giochi.

Attività pratica: la realizzazione di un automa per validare numeri interi positivi, la simulazione degli stati di un personaggio in un videogioco, e la creazione di un prototipo di videogioco con controllo del personaggio.

Nel mondo dell’informatica teorica, i linguaggi formali e gli automi a stati finiti (Finite State Machines, FSM) rappresentano strumenti fondamentali per comprendere come funzionano la computazione, la validazione dei dati e persino alcune logiche di gioco. Anche se spesso vengono percepiti come concetti astratti, possono rivelarsi estremamente pratici nella didattica, soprattutto se tradotti in attività laboratoriali concrete.

Gli automi a stati finiti (ASF) sono modelli matematici usati per rappresentare comportamenti sequenziali. Il loro scopo è descrivere un sistema che, in ogni momento, si trova esattamente in uno di un numero finito di stati possibili. Gli ASM sono capaci di:

  • Rappresentare comportamenti sequenziali (es. stati di un personaggio di gioco: “fermo”, “cammina”, “salta”…)
  • Validare stringhe di input (es. controllare se un identificatore è valido, o se una password rispetta un formato)
  • Modellare sistemi reattivi (es. menù interattivi, sistemi di controllo, semafori)
  • Semplificare la progettazione e il debugging: uno stato alla volta, nessuna ambiguità

Un automa a stati finiti è composto da:

  • un insieme finito di stati
    - Tutte le possibili condizioni in cui il sistema si può trovare
  • uno stato iniziale
    - Lo stato in cui il sistema si trova all’inizio.
  • uno o più stati finali (a seconda del contesto)
    - Sono opzionali e indicano un risultato valido o completato.
  • un alfabeto di simboli in ingresso
    - L’insieme di tutti i possibili eventi o simboli che causano un cambiamento
  • una funzione di transizione
    - Regola che indica come cambiare stato in base all’input

Una possibile rappresentazione grafica è un diagramma o grafo orientato: ciascun nodo è uno stato, e le frecce etichettate indicano le transizioni.

In questo esempio l’ASF ha 3 stati: Stato 1, Stato 2 e Stato 3 (stato finale). La Transizione A porta l’ASF dallo Stato 1 allo Stato 2, la Transizione B porta l’ASF dallo Stato 2 allo Stato1 e la Transizione C porta l’ASF dallo Stato 2 allo Stato 3.

Esempi pratici di ASF

1 - Il Semaforo Stradale

Questo è l'esempio classico di un sistema reattivo modellato da un ASF.

  • Stati: Rosso, Verde, Giallo.
  • Input (Alfabeto): Un segnale di Tempo Scaduto (il timer interno).
  • Funzione di Transizione:
  • DA Rosso + Input Tempo Scaduto  A Verde
  • DA Verde + Input Tempo Scaduto  A Giallo
  • DA Giallo + Input Tempo Scaduto  A Rosso

In questo modello, lo stato attuale determina il prossimo stato, garantendo un ciclo di funzionamento coerente e non ambiguo.

2 – Il distributore automatico

Un distributore automatico è un sistema che deve ricordare lo stato corrente dei soldi inseriti per determinare le azioni successive.

Stati 

Gli stati rappresentano l'ammontare totale di credito inserito e definiscono ciò che l'utente può fare:

  • S0 (Iniziale): Nessun Credito (0€). La macchina è in attesa di monete.
  • S1 Credito Parziale (ad esempio, 0.50€). Non sufficiente per il prodotto.
  • S2 (Finale): Credito Sufficiente (ad esempio, 1.00€ o più). Il prodotto può essere erogato

Alfabeto di Input

Gli input sono gli eventi che provocano le transizioni:

  • M050: Inserimento di una moneta da 0.50€.
  • M100: Inserimento di una moneta da 1.00€.
  • B: Pressione del Pulsante di Acquisto (se il credito è sufficiente).
  • R: Pressione del Pulsante di Ritiro Resto.

Funzione di Transizione: Regole di Funzionamento

Le transizioni determinano come si muove il sistema tra gli stati.

Rappresentazione mediante grafo

Attività pratica n.1

Validazione dell’input

La validazione dell’input è uno dei contesti più efficaci per far comprendere agli studenti il potere dei linguaggi formali e degli automi a stati finiti. Molti programmatori usano subito le espressioni regolari per controllare il formato di un dato, ma spesso non capiscono davvero perché funzionano o che cosa rappresentano.

Usare una FSM permette invece di far emergere:

  • la logica formale dietro la validazione
  • il concetto di riconoscimento di linguaggi
  • l’utilità pratica degli automi nella progettazione di software robusto

Implementiamo un automa a stati finiti che riconosca numeri interi positivi, ovvero stringhe costituite da una o più cifre (0–9) con:

  • nessuno spazio
  • nessuna lettera
  • nessun simbolo
  • almeno una cifra

Esempi accettati:"0","5","123","999999"
Esempi rifiutati:""," 123","12 34","-5","12.5","3a7"


Progetto dell’automa

Stati

  • S – stato iniziale
    Attende la prima cifra
  • D – “dentro numero”
    Dopo aver letto almeno una cifra
  • X – stato di errore
    Se arriva un carattere non valido
  • D è stato accettante

Transizioni

Grafo

Codice Python
Ecco una versione della FSM completamente implementata (input.py)

# Insieme degli stati dell’automa:
# S = stato iniziale
# D = stato "dentro numero" (accettante)
# X = stato di errore
states = {"S", "D", "X"}

# Stato iniziale
start = "S"

# Stati accettanti (qui solo D)
accepting = {"D"}

def get_symbol(c):
    # Restituisce il tipo di simbolo letto.
    # L'automa non lavora direttamente sui caratteri,
    # ma su categorie astratte: 'digit' o 'other'.
    return "digit" if c.isdigit() else "other"

# Funzione di transizione:
# (stato_corrente, simbolo) → nuovo_stato
transitions = {
    ("S", "digit"): "D",    # La prima cifra rende il numero valido
    ("S", "other"): "X",    # Se già il primo carattere non è cifra → errore

    ("D", "digit"): "D",    # Altre cifre mantengono la validità
    ("D", "other"): "X",    # Qualunque carattere non numerico dopo cifre → errore

    ("X", "digit"): "X",    # Una volta in errore, si rimane in errore
    ("X", "other"): "X",
}

def run_fsm(s):
    # Esegue l'automa sulla stringa s.
    # Ritorna True se s viene riconosciuta come numero intero positivo.

    # Lo stato corrente parte sempre dallo stato iniziale
    state = start

    # Analizza la stringa carattere per carattere
    for c in s:

        # Converte il carattere in un 'simbolo' astratto
        symbol = get_symbol(c)

        # Applica la transizione definita nell'automa
        state = transitions[(state, symbol)]

    # La stringa è accettata solo se lo stato finale è uno stato accettante
    return state in accepting

# -------------------------
# TEST DI VALIDAZIONE
# -------------------------

print(run_fsm("12345"))  # True: tutte cifre
print(run_fsm("12a45"))  # False: contiene una lettera
print(run_fsm(""))       # False: stringa vuota → rimane in S (non accettante)
print(run_fsm("0"))      # True: singola cifra è valida
print(run_fsm(" 123"))   # False: spazio iniziale → errore
print(run_fsm("42 "))    # False: spazio finale → errore

Attività pratica n.2

Controllare il personaggio di un videogioco

Immaginiamo un semplice videogioco 2D di tipo platform. Il personaggio può:

  • stare fermo (Idle)
  • camminare (Walk)
  • saltare (Jump)
  • subire un danno (Hit)
  • essere KO (KO)

Ogni stato definisce cosa il personaggio sta facendo e quali transizioni sono possibili.

Stati possibili e logica

1. Idle
Il personaggio non sta compiendo azioni.
Transizioni:

  • Idle → Walk (se viene premuto un tasto di movimento)
  • Idle → Jump (se si preme il tasto “salta”)
  • Idle → Hit (se si subisce un attacco)

2. Walk
Il personaggio si muove orizzontalmente.
Transizioni:

  • Walk → Idle (se si rilasciano i tasti di movimento)
  • Walk → Jump (se si preme “salta”)
  • Walk → Hit (se colpito)

3. Jump
Il personaggio è in aria.
Transizioni:

  • Jump → Idle (quando atterra)
  • Jump → Hit (se viene colpito in aria)

4. Hit
Il personaggio è colpito e riproduce un’animazione di danno.
Transizioni:

  • Hit → Idle (se resta HealthPoints > 0 e finisce l’animazione)
  • Hit → KO (se HealthPoints <= 0)

5. KO
Il personaggio è sconfitto. Stato finale.
Transizioni:

  • KO → (nessuna)

Grafo

Codice Python

Il codice seguente (character.py) propone una struttura semplice da far implementare agli studenti.
Non è un motore di gioco completo, ma una base concettuale.

class CharacterFSM:
    def __init__(self):
        self.state = "Idle"
        self.hp = 3

    def handle_event(self, event):
        print(f"Stato attuale: {self.state}. Evento: {event}")

        if self.state == "Idle":
            if event == "move":
                self.state = "Walk"
            elif event == "jump":
                self.state = "Jump"
            elif event == "hit":
                self.state = "Hit"

        elif self.state == "Walk":
            if event == "stop":
                self.state = "Idle"
            elif event == "jump":
                self.state = "Jump"
            elif event == "hit":
                self.state = "Hit"

        elif self.state == "Jump":
            if event == "land":
                self.state = "Idle"
            elif event == "hit":
                self.state = "Hit"

        elif self.state == "Hit":
            self.hp -= 1
            if self.hp <= 0:
                self.state = "KO"
            else:
                self.state = "Idle"

        print(f"Nuovo stato: {self.state}\n")

char = CharacterFSM()
char.handle_event("move")
char.handle_event("jump")
char.handle_event("land")
char.handle_event("hit")
char.handle_event("hit")
char.handle_event("hit")


Esecuzione

Stato attuale: Idle. Evento: move
Nuovo stato: Walk

Stato attuale: Walk. Evento: jump
Nuovo stato: Jump

Stato attuale: Jump. Evento: land
Nuovo stato: Idle

Stato attuale: Idle. Evento: hit
Nuovo stato: Hit

Stato attuale: Hit. Evento: hit
Nuovo stato: Idle

Stato attuale: Idle. Evento: hit
Nuovo stato: Hit

Attività pratica n.3

Prototipo di videogioco con controllo del personaggio

Il codice seguente (gameplay.py) propone la realizzazione di un prototipo di videogioco con personaggio controllato dallo stesso FSM considerato nell’attività pratica numero 2. Per l’esecuzione è necessario installare pygame (pip install pygame).

Comportamenti implementati:

  • Stati: Idle, Walk, Jump, Hit, KO
  • Tasti:
    - Frecce sinistra/destra → movimento
    - Barra spaziatrice → salto
  • Gravità e atterraggio simulati
  • Colpo che riduce HP e può mandare KO

Il programma visualizza un personaggio come un semplice rettangolo che cambia colore in base allo stato.

  • Idle (grigio) → fermo
  • Walk (blu) → si muove
  • Jump (giallo) → salta
  • Hit (rosso) → colpito
  • KO (rosso scuro) → sconfitto

Il log della console mostra tutte le transizioni.

Per rendere dinamico il gioco è stato aggiunto

  • Un nemico rappresentato da un quadrato rosso scuro che si muove avanti e indietro.
  • Una funzione di collisione tra personaggio ed enemy.
  • Quando c’è collisione → la FSM del personaggio riceve l’evento "hit".

Codice Python

import pygame
import sys

# -----------------------
#      FSM DEL PERSONAGGIO
# -----------------------
class CharacterFSM:
    def __init__(self):
        # Stato iniziale del personaggio
        self.state = "Idle"
        # Punti vita
        self.hp = 10
        # Velocità verticale usata per la fisica del salto
        self.y_velocity = 0
        # Flag che indica se il personaggio è a terra (può saltare)
        self.is_on_ground = True

    def handle_event(self, event):
        # Stampa evento e stato corrente (utile per debug)
        print(f"EVENTO: {event}, Stato: {self.state}")

        # LOGICA DEGLI STATI: transizioni basate sull'evento ricevuto
        if self.state == "Idle":
            if event == "move":
                # Quando ci si muove da fermi -> camminare
                self.state = "Walk"
            elif event == "jump":
                # Inizio salto
                self.state = "Jump"
            elif event == "hit":
                # Subito dopo essere colpiti
                self.state = "Hit"

        elif self.state == "Walk":
            if event == "stop":
                # Stop movimento -> tornare fermi
                self.state = "Idle"
            elif event == "jump":
                # Saltare anche mentre si cammina
                self.state = "Jump"
            elif event == "hit":
                # Colpito mentre cammina
                self.state = "Hit"

        elif self.state == "Jump":
            if event == "land":
                # Atterraggio -> tornare a Idle
                self.state = "Idle"
            elif event == "hit":
                # Colpito in aria
                self.state = "Hit"

        elif self.state == "Hit":
            # Quando si entra nello stato Hit si decrementano gli HP
            self.hp -= 1
            if self.hp <= 0:
                # Se gli HP finiscono -> KO permanente
                self.state = "KO"
            else:
                # Altrimenti si torna a Idle dopo l'animazione di hit
                self.state = "Idle"

        # Stampa il nuovo stato (utile per debug)
        print(f" → Nuovo stato: {self.state}")

# -----------------------
#      SETUP DI GIOCO
# -----------------------
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()

# Posizione iniziale del personaggio (x, y)
x = 400
y = 500
# Velocità orizzontale del personaggio
speed = 5
# Accelerazione dovuta alla gravità usata per il salto
gravity = 0.8
# Istanza della macchina a stati del personaggio
fsm = CharacterFSM()

# -----------------------
#      NEMICO
# -----------------------
# Posizione del nemico
enemy_x = 200
enemy_y = 500
# Velocità di movimento del nemico e direzione (1 destra, -1 sinistra)
enemy_speed = 3
enemy_direction = 1  # 1 = destra, -1 = sinistra

# -----------------------
#   LOOP PRINCIPALE
# -----------------------
while True:
    # Lettura input tastiera
    keys = pygame.key.get_pressed()
    moved = False  # flag per capire se il personaggio si è mosso orizzontalmente

    # Gestione eventi di sistema (es. chiusura finestra)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

    # --- MOVIMENTO ORIZZONTALE ---
    if keys[pygame.K_LEFT]:
        x -= speed
        moved = True
    if keys[pygame.K_RIGHT]:
        x += speed
        moved = True

    # Transizioni tra Idle <-> Walk in base al movimento orizzontale
    if moved and fsm.state == "Idle":
        fsm.handle_event("move")
    if not moved and fsm.state == "Walk":
        fsm.handle_event("stop")

    # --- SALTO ---
    # Se premi SPACE e sei a terra, inizia il salto
    if keys[pygame.K_SPACE] and fsm.is_on_ground:
        fsm.is_on_ground = False
        # Impulso iniziale verso l'alto (valore negativo per y decresce verso l'alto nello schermo)
        fsm.y_velocity = -15
        fsm.handle_event("jump")

    # ----------------------------
    #   FISICA DEL SALTO
    # ----------------------------
    if not fsm.is_on_ground:
        # Aggiorno posizione verticale in base alla velocità verticale
        y += fsm.y_velocity
        # Applico gravità incrementando la velocità verso il basso
        fsm.y_velocity += gravity

        # Rilevo atterraggio sul "terreno" (y = 500)
        if y >= 500:
            y = 500
            fsm.is_on_ground = True
            fsm.y_velocity = 0
            # Al ritorno a terra, se ero in Jump genero evento land
            if fsm.state == "Jump":
                fsm.handle_event("land")

    # ----------------------------
    #   MOVIMENTO DEL NEMICO
    # ----------------------------
    # Sposta il nemico e inverte direzione ai limiti
    enemy_x += enemy_speed * enemy_direction

    if enemy_x <= 100 or enemy_x >= 650:  # limiti di pattuglia del nemico
        enemy_direction *= -1

    # ----------------------------
    #   COLLISIONE PERSONAGGIO / NEMICO
    # ----------------------------
    # Rette di collisione (possono essere sostituite da hitbox più precise)
    player_rect = pygame.Rect(x, y, 50, 80)
    enemy_rect = pygame.Rect(enemy_x, enemy_y, 50, 80)

    # Se c'è collisione e il personaggio non è già KO, riceve un colpo
    if player_rect.colliderect(enemy_rect) and fsm.state != "KO":
        fsm.handle_event("hit")

    # ----------------------------
    #   RENDER
    # ----------------------------
    # Pulisco lo schermo
    screen.fill((30, 30, 30))

    # Mappa colori per visualizzare lo stato corrente del personaggio
    colors = {
        "Idle": (200, 200, 200),  # Grigio chiaro
        "Walk": (50, 150, 255),   # Blu    
        "Jump": (255, 200, 50),   # Giallo
        "Hit": (255, 80, 80),     # Rosso chiaro
        "KO": (80, 0, 0)          # Rosso scuro
    }

    # Disegno del personaggio con colore in base allo stato
    pygame.draw.rect(screen, colors[fsm.state], player_rect)

    # Disegno del nemico
    pygame.draw.rect(screen, (180, 0, 0), enemy_rect)

    # Testo informativo in alto a sinistra (stato e HP)
    font = pygame.font.SysFont(None, 32)
    text = font.render(f"Stato: {fsm.state}   HP: {fsm.hp}", True, (255, 255, 255))
    screen.blit(text, (10, 10))

    # Aggiorno schermo e limito il frame rate
    pygame.display.flip()
    clock.tick(60)

Indice

Azioni

Stampa Articolo