Contenuti

Hackathon Linux Day 2022

In questo post vi racconterò come ho vinto l’hackathon del Linux Day 2022.

In realtà non l’ho vinto tutto da solo, ma con l’aiuto del mio team composto da me, Michelino e Peppe.

La challenge, organizzata dalla par-tec, consisteva nel progettare e implementare un agente che giocasse a battlesnake.

Per chi non conoscesse battlesnake (io in primis non l’avevo mai sentito nominare prima) è un gioco dove ogni giocatore implementa delle (web) api che consentono al proprio snake di giocare contro gli altri.
L’ultimo che sopravvive, vince!

Una run di battlesnake. Gif presa dal sito.

Le regole del gioco sono:

  1. Ad ogni turno, ogni snake dovrà decidere (dato lo stato interno della mappa) quale mossa fare. Se non risponde, o se risponde in maniera non conforme alle interfacce, verrà eliminato.
  2. Ogni serpente ha una vita, indicata con un numero da 0 a 100. Ad ogni turno la vita cala di una certa quantità. Se la vita scende a 0, il serpente muore.
  3. Per ripristinare la vita bisogna mangiare uno dei frutti presenti sul tavolo.
  4. Se la testa di un serpente collide con il corpo di un altro (o con il proprio) oppure contro uno dei bordi, esso muore.
  5. L’ultimo serpente in vita vince la battlesnake!

Fase 1 - Setup

La prima fase della challeng consisteva nel istanziare un agente, in maniera coerente con lo standard.

Una passeggiata!
Questo era la nostra configurazione.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def info() -> typing.Dict:
    print("INFO")

    return {
        "apiversion": "1",
        "author": "MrPeroni",  # TODO: Your Battlesnake Username
        "color": "#297516",  # TODO: Choose color
        "head": "safe",  # TODO: Choose head
        "tail": "freckled",  # TODO: Choose tail
    }

Il nome del nostro eroe era MrPeroni, da cui il nome del team: Team Peroni.

Il nostro eroe MrPeroni in tutto il suo splendore.

Fase 2 - First steps

La seconda fase consisteva nello scrivere un primo agente (banale) che sopravvivesse (in solitaria) almeno 100 turni.

Dato che a disposizione avevamo pochissimo tempo (meno di un’ora se non ricordo male) abbiamo tutti quanti adottato l’aproccio “cerca di non morire”, oppure “evita gli ostacoli”. Banalmente, la strategia era questa:

  1. Guardati intorno.
  2. Tra le 3 possibili direzioni, scarta quelle pericolose.
  3. Se ci sono mosse caute disponibili, scegline una a caso, altrimenti muori :(

Facile no?

Per far compiere l’azione a MrPeroni, bisognava esporre una api che rispondesse quale mossa fare. Infatti, l’istanza del gioco, fa ad ogni turno una richiesta POST ad ogni agente, passando un json contenente lo stato globale della mappa.

1
2
3
4
@app.post("/move")
def on_move():
    game_state = request.get_json()
    return MrPeroni.move(game_state)

Il json game_state è composto dalle seguenti informazioni:

  • board: descrive tutte le informazioni sul “campo di battaglia”:
    • food: la lista delle coordinate in cui si trovano i frutti da mangiare.
    • hazards: la lista delle coordinate in cui si trovano gli eventuali muri.
    • snakes: la lista che contiene tutte le informazioni sugli altri player. Utile per sapere dove sono i corpi degli altri serpenti (da evitare).
    • height: l'altezza della griglia.
    • width: la larghezza della griglia.
  • game: metadati sulla partita (non particolarmente utili).
  • turn: il turno corrente.
  • you: descrive tutte le informazioni sul tuo agente:
    • body: la lista di coordinate del corpo del serpente. Per fortuna ordinate dalla testa alla coda.
    • customizations: lo stile dello snake.
    • head: le coordinate della testa, ovvero you.body[0].
    • health: vita (da 0 a 100) del serpente.
    • id: numero seriale identificativo.
    • length: lunghezza del serpente, ovvero you.body.length.
    • altro…
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
{
    "board": {
        "food": [
            {
                "x": 0,
                "y": 2
            },
            {
                "x": 5,
                "y": 5
            }
        ],
        "hazards": [],
        "height": 11,
        "snakes": [
            {
                "body": [
                    {
                        "x": 2,
                        "y": 0
                    },
                    {
                        "x": 2,
                        "y": 1
                    },
                    {
                        "x": 1,
                        "y": 1
                    }
                ],
                "customizations": {
                    "color": "#297516",
                    "head": "safe",
                    "tail": "freckled"
                },
                "head": {
                    "x": 2,
                    "y": 0
                },
                "health": 98,
                "id": "728a60c4-c28c-4676-bfd2-86df05c6d400",
                "latency": "0",
                "length": 3,
                "name": "Python Starter Project",
                "shout": "",
                "squad": ""
            }
        ],
        "width": 11
    },
    "game": {
        "id": "497d9fd7-f6f1-4855-9b12-af760c220f41",
        "map": "standard",
        "ruleset": {
            "name": "solo",
            "settings": {
                "foodSpawnChance": 15,
                "hazardDamagePerTurn": 14,
                "hazardMap": "",
                "hazardMapAuthor": "",
                "minimumFood": 1,
                "royale": {
                    "shrinkEveryNTurns": 25
                },
                "squad": {
                    "allowBodyCollisions": false,
                    "sharedElimination": false,
                    "sharedHealth": false,
                    "sharedLength": false
                }
            },
            "version": "cli"
        },
        "source": "",
        "timeout": 500
    },
    "turn": 2,
    "you": {
        "body": [
            {
                "x": 2,
                "y": 0
            },
            {
                "x": 2,
                "y": 1
            },
            {
                "x": 1,
                "y": 1
            }
        ],
        "customizations": {
            "color": "#297516",
            "head": "safe",
            "tail": "freckled"
        },
        "head": {
            "x": 2,
            "y": 0
        },
        "health": 98,
        "id": "728a60c4-c28c-4676-bfd2-86df05c6d400",
        "latency": "0",
        "length": 3,
        "name": "Python Starter Project",
        "shout": "",
        "squad": ""
    }
}

A questo punto, l’algoritmo da implementare è abbastanza semplice.

  • Step 0. Scarto la mossa che farebbe collidere la testa di MrPeroni col suo collo.
  • Step 1. Scarto tutte le mosse che farebbero uscire MrPeroni dalla mappa.
  • Step 2. Scarto le mosse che farebbero collidere MrPeroni con se stesso (caso generale dello Step 0).
  • Final Step. Se ci sono mosse rimanenti, ne scelgo una a caso, altrimenti decido la mossa vai giù (tanto qualsiasi mossa porterebbe MrPeroni verso la morte).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def move(game_state: typing.Dict) -> typing.Dict:

    is_move_safe = {"up": True, "down": True, "left": True, "right": True}

    my_head = game_state["you"]["body"][0]  # Coordinates of your head
    my_neck = game_state["you"]["body"][1]  # Coordinates of your "neck"

    if my_neck["x"] < my_head["x"]:  # Neck is left of head, don't move left
        is_move_safe["left"] = False

    elif my_neck["x"] > my_head["x"]:  # Neck is right of head, don't move right
        is_move_safe["right"] = False

    elif my_neck["y"] < my_head["y"]:  # Neck is below head, don't move down
        is_move_safe["down"] = False

    elif my_neck["y"] > my_head["y"]:  # Neck is above head, don't move up
        is_move_safe["up"] = False

    # Step 1 - Prevent your Battlesnake from moving out of bounds
    board_width = game_state['board']['width']
    board_height = game_state['board']['height']

    if my_head["x"] == 0: # don't go left
        is_move_safe["left"] = False
    if my_head["x"] == board_width-1: # don't go right
        is_move_safe["right"] = False

    if my_head["y"] == 0: # don't go down
        is_move_safe["down"] = False
    if my_head["y"] == board_height-1: # don't go up
        is_move_safe["up"] = False
    
    # Step 2 - Prevent your Battlesnake from colliding with itself
    my_body = game_state['you']['body']

    def hit(x, y, body) -> bool:
        """
            Check if a point (`x`, `y`) hits a body
        """
        for cell in body:
            if x == cell["x"] and y == cell["y"]:
                return True
        return False
    
    is_move_safe["up"] = (not hit(my_head["x"], my_head["y"]+1, my_body)) and is_move_safe["up"] 
    is_move_safe["down"] = (not hit(my_head["x"], my_head["y"]-1, my_body)) and is_move_safe["down"] 
    is_move_safe["left"] = (not hit(my_head["x"]-1, my_head["y"], my_body)) and is_move_safe["left"] 
    is_move_safe["right"] = (not hit(my_head["x"]+1, my_head["y"], my_body)) and is_move_safe["right"] 

    # Are there any safe moves left?
    safe_moves = []
    for move, isSafe in is_move_safe.items():
        if isSafe:
            safe_moves.append(move)

    if len(safe_moves) == 0:
        print(f"MOVE {game_state['turn']}: No safe moves detected! Moving down")
        return {"move": "down"}

    # Choose a random move from the safe ones
    next_move = random.choice(safe_moves)

    print(f"MOVE {game_state['turn']}: {next_move}")
    return {"move": next_move}

Ho deciso di non implementare un controllo per evitare gli altri serpenti perché in questo step ancora non ce ne sono: si gioca in solitaria.

Alcuni di voi potrebbero pensare:

“Ma perché, tra le mosse disponibili, ne scegli una totalmente a caso? Se c’è un cibo vicino, perché non mangiarlo?"

Beh potrebbe essere una strategia ragionevole quella di scegliere, tra le mosse disponibili, quella di mangiare (se presente del cibo nelle vicinanze). Anche perché la vita del serpente diminuisce di turno in turno se non si mangia, e quando arriva a 0 si perde.

Però, euristicamente, abbiamo notato che mangiando sempre si moriva più in fretta. Infatti, mangiando troppo spesso MrPeroni diventava lungo molto velocemente, e quando era troppo lungo facilmente finiva per incastrarsi da solo!

Visto che comunque muovendosi a caso ogni tanto capitava di mangiare, era davvero poco probabile morire di fame. E dato che la crescita di MrPeroni era “rallentata”, la sua durata di vita migliorava.

Ed ecco che a mani basse passiamo anche il secondo turno, entrando a gamba tesa nella fase finale della gara!

Fase 3 - Facciamo sul serio

Arrivati alla fase finale bisognava far combattare tra di loro le diverse squadre!

Dato che ci siamo presi una bella pausa pranzo, avevamo a disposizione poco più di un’ora e mezza per migliorare MrPeroni in modo tale da poter essere minimamente competitivo.

Da buon informatico appassionato di algoritmi, sono partito dal modellare lo stato della mappa come un grafo.

     Tutto è un grafo!

Step 1

Come prima cosa ho rappresentato il campo di gioco come un grafo a griglia, dove per ogni cella $(i,j)$ c’è un nodo $v_{i,j}$, e per ogni coppia di celle **adiacenti** $(i,j), (h,k)$ c’è un arco $(v_{i,j}, v_{h,k})$.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import itertools
import networkx as nx

class Agent:
    def __init__(self, game_state):
        self.head = game_state["you"]["head"]
        self.board = game_state["board"]

        self.graph_grid = nx.grid_2d_graph(self.board["width"], self.board["height"])
        # ...

Step 2

Dopodiché, rimuoviamo tutti i nodi che possono rappresentare ostacoli.

1
2
3
4
5
6
7
8
class Agent:
    def __init__(self, game_state):
        # ...

        for snake in self.board.snakes:
            self.graph_grid.remove_nodes_from([(c["y"], c["x"]) for c in snake["body"]])

        # ...
Avvertimento

Anche il corpo di MrPeroni stesso può rappresentare un ostacolo, infatti deve essere rimosso. L’unico pezzo di corpo che NON deve essere rimosso è la testa di MrPeroni. Infatti la sua testa non deve essere rimossa per due motivi:

  1. La testa di MrPeroni non può schiantarsi sulla sua stessa casella.
  2. Il nodo rispettivo alla testa di MrPeroni ci sarà utile come punto di partenza per una visita sul grafo della mappa di gioco.

Il codice corretto è quindi

1
2
3
4
5
6
7
8
# ...
for snake in self.board.snakes:
    self.graph_grid.remove_nodes_from([
        (c["y"], c["x"])
        for c in snake["body"]
        if c != self.head
        ])
# ...

Step 3

Modellizzata la nostra istanza, siamo ora in grado di calcolare la mossa da fare ad ogni turno.

L’idea di base è molto semplice:

  1. faccio partire una visita in ampiezza radicata nel nodo $v_{\text{head}}$ che rappresenta la **testa** di MrPeroni. Dato che il grafo è **non pesato**, allora l'**albero BFS** risultante equivale esattamente allo **Shortest Path Tree**.
  2. Se nel BFS-tree risultante è presente almeno un nodo cibo $x$, allora come mossa successiva percorriamo il primo passo del cammino minimo $v_{\text{head}} \leadsto x$.
  3. Se invece nessun nodo cibo è raggiungibile non ci resta che adottare la politica “cerca di non morire finché un cibo non risulterà raggiungibile”.

Esempio di BFS-tree con radice (5,3), indicato con una stella.

Nella figura in esempio ci sono ben 3 cibi raggiungibili. Scegliamone uno a caso, per esempio quello in coordinata $(0,1)$.
La mossa di MrPeroni in questo turno sarà quindi UP.

Euristica

Facendo alcuni test abbiamo notato che MrPeroni tendeva a morire più frequentemente quando era vicino ai bordi. Infatti, se il serpente è vicino ai bordi è più probabile che finisca in un “vicolo cieco”, andando così incontro alla sua morte.

Perciò come euristica abbiamo pensato che sarebbe stato più ragionevole scegliere di mangiare i cibi nella regione più centrale della mappa.

Applicando questa euristica abbiamo notato un notevole incremento del 25%-30% nella longevità di MrPeroni.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def scores(self):
    """
        Reuturns a list of food points ordered by the distance from the center of the board.
    """
    return sorted(
        self.board["food"],
        key=lambda p: math.dist(
            (p["x"], p["y"]),
            (self.board["width"]/2, self.board["height"]/2)
        )
    )

def compute_step(self):
    """
        Compute the next step of the snake.
    """

    # Compute the Shortest Path Tree
    spt = nx.shortest_path(self.graph_grid, (self.head["x"], self.head["y"]))

    # If there is a food point on the board
    if len(self.board["food"]) != 0:

        # Get the most central food point
        for food_point in self.scores(): # euristica
            i, j = food_point["x"], food_point["y"]
            target_point = (i,j)

            # If the target point is reachable from the head
            if target_point in spt.keys():
                # Get the next step
                return spt[target_point][1]
    
    # If there is no food point on the board
    # or any food point is not reachable from the head
    # return none move
    return None

Possibili miglioramenti

Anche se ci si avvicina parecchio, MrPeroni non è un essere perfetto: può essere migliorato!.

Un’ulteriore euristica che si può adottare è quella precedente del “mangiare poco”, ovvero non buttarsi in maniera aggressiva sul cibo ad ogni turno.
Nello specifico sarebbe più ragionevole andare alla ricerca di cibo solamente quando la vita di MrPeroni scende sotto una certa soglia critica (per esempio sotto il 75%), evitando quanto possibile il cibo.

Un’altra euristica invece si basa sulla scelta del cibo da raggiungere. MrPeroni usa la politica “il più vicino al centro/lontano dai bordi”, però non è l’unica polita ragionevole. Osserviamo che l’albero bfs radicato sulla testa del serpente ha al più 3 sottoalberi, perché dalle mosse “safe” dobbiamo rimuovere per forza il collo. Un polita ragionevole potrebbe quindi essere quella di scegliere, tra i sottoalberi che contengono del cibo, il sottoalbero più grande. L’idea è che se mi dirigo in un sottoalbero piccolo potrei finire facilmente in un vicolo cieco.

Insomma, ci possono essere una infinità di miglioramente che potevamo fare al nostro eroe, però purtroppo il tempo era poco (circa un ora e mezza) e questo è il meglio che siamo riusciti a progettare, implementare e testare.

Se hai qualche idea su come poterlo migliorare sei invitato a contattarmi, e ne discuteremo davanti a una birra (offro io).

Final code

Il codice finale della funzione move è una cosa del genere:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def move(game_state: typing.Dict) -> typing.Dict:
    my_head = game_state["you"]["body"][0]  # Coordinates of your "head"
    my_neck = game_state["you"]["body"][1]  # Coordinates of your "neck"

    # Instantiate the agent
    from agent import Agent
    MR_PERONI = Agent(game_state)

    # Compute the next step
    next_step = MR_PERONI.compute_step()

    # If the agent is able to compute the next step
    if next_step != None:
        x, y = next_step

        if x == my_head["x"] + 1:
            return {"move": "right"}
        elif x == my_head["x"] - 1:
            return {"move": "left"}
        elif y == my_head["y"] + 1:
            return {"move": "up"}
        elif y == my_head["y"] - 1:
            return {"move": "down"}

    # If the agent is not able to compute the next step
    # compute the next step according the policy 'avoid obstacles'

    is_move_safe = {"up": True, "down": True, "left": True, "right": True}

    if my_neck["x"] < my_head["x"]:  # Neck is left of head, don't move left
        is_move_safe["left"] = False

    elif my_neck["x"] > my_head["x"]:  # Neck is right of head, don't move right
        is_move_safe["right"] = False

    elif my_neck["y"] < my_head["y"]:  # Neck is below head, don't move down
        is_move_safe["down"] = False

    elif my_neck["y"] > my_head["y"]:  # Neck is above head, don't move up
        is_move_safe["up"] = False

    # Step 1 - Prevent your Battlesnake from moving out of bounds
    board_width = game_state['board']['width']
    board_height = game_state['board']['height']

    if my_head["x"] == 0: # don't go left
        is_move_safe["left"] = False
    if my_head["x"] == board_width-1: # don't go right
        is_move_safe["right"] = False

    if my_head["y"] == 0: # don't go down
        is_move_safe["down"] = False
    if my_head["y"] == board_height-1: # don't go up
        is_move_safe["up"] = False
    
    # Step 2 - Prevent your Battlesnake from colliding with itself
    my_body = game_state['you']['body']

    def hit(x, y, body) -> bool:
        """
            Check if a point (`x`, `y`) hits a body
        """
        for cell in body:
            if x == cell["x"] and y == cell["y"]:
                return True
        return False
    
    is_move_safe["up"] = (not hit(my_head["x"], my_head["y"]+1, my_body)) and is_move_safe["up"] 
    is_move_safe["down"] = (not hit(my_head["x"], my_head["y"]-1, my_body)) and is_move_safe["down"] 
    is_move_safe["left"] = (not hit(my_head["x"]-1, my_head["y"], my_body)) and is_move_safe["left"] 
    is_move_safe["right"] = (not hit(my_head["x"]+1, my_head["y"], my_body)) and is_move_safe["right"] 

    # Step 3 - Prevent your Battlesnake from colliding with other Battlesnakes
    opponents = game_state['board']['snakes']

    for opponent in opponents:
        if opponent["id"] != game_state["you"]["id"]:
            is_move_safe["up"] = (not hit(my_head["x"], my_head["y"]+1, opponent["body"])) and is_move_safe["up"] 
            is_move_safe["down"] = (not hit(my_head["x"], my_head["y"]-1, opponent["body"])) and is_move_safe["down"] 
            is_move_safe["left"] = (not hit(my_head["x"]-1, my_head["y"], opponent["body"])) and is_move_safe["left"] 
            is_move_safe["right"] = (not hit(my_head["x"]+1, my_head["y"], opponent["body"])) and is_move_safe["right"]

    # Are there any safe moves left?
    safe_moves = []
    for move, isSafe in is_move_safe.items():
        if isSafe:
            safe_moves.append(move)

    if len(safe_moves) == 0:
        print(f"MOVE {game_state['turn']}: No safe moves detected! Moving down")
        return {"move": "down"}

    # Choose a random move from the safe ones
    next_move = random.choice(safe_moves)

    print(f"MOVE {game_state['turn']}: {next_move}")
    return {"move": next_move}
Pericolo
Et voilat, MrPeroni è pronto a fare una strage!

La resa dei conti

È arrivato il momento che MrPeroni dimostri la sua superiorità!

Non ci sono parole per la magnificenza di MrPeroni, quindi lascio che le testimonianze video parlino.

L’emozione di vedere MrPeroni, la mia creatura malvagia, distruggere letteralmente la concorrenza è stato indescrivibile.

Ma l’emozione più grande non è conseguenza della gloria della vittoria, quanto nell'umiliazione dei “nemici”. Infatti una cosa non vi ho ancora detto: eravamo nella struttura di ingegneria contro tutti ingegneri informatici!

Eravamo in territorio nemico e circondati, però ne siamo usciti vincitori nel migliore dei modi: 9 partite vinte su 10, di cui l'unica persa è stato perché MrPeroni si è “ammazzato” da solo (un auto goal insomma).

Informatica vs Ing. Informatica

Nessuno vuole ammetterlo pubblicamente, ma la differenza tra informatici ed ingegneri informatici (in italia) esiste!

La vera differenza non sta nelle conoscenze, perché bene o male noi e gli ingegneri informatici studiamo più o meno le stesse cose, ma nel mindset cln cui affrontiamo l’informatica.

Noi informatici, essendo una branca della matematica, studiamo in maniera rigorosa e formale l’informatica, soprattutto gli algoritmi. L’ingengere fa invece uno studio degli algoritmi meno formale, concentrandosi di più sul loro utilizzo: come si dice in questi casi fanno un utilizzo black box degli algoritmi.

Dato che da quando ho iniziato a studiare informatica campo a “pane e grafi”, mi è venuto immediato e naturale modellizzare il problema come un problema di visita su grafi. Cosa che nessuno degli altri concorrenti ha fatto, nemmeno gli esperti della par-tec che hanno voluto partecipare.

Non che la mia sia una soluzione geniale. Tuttaltro, è una idea banalissima!
Andando a vedere i codici degli altri team che hanno partecipato, altro non erano che una seria di if-then-else statements: erano un enorme switch-case code!

Non voglio assolutamente criticare gli ingegneri informatici, anche perché ne conosco alcuni di cui nutro un profondo rispetto. Allo stesso modo non voglio esaltare gli informatici: conosco informatici il cui rispetto (professionale) da perte mia è meno che nullo…

È inutile anche criticare la disciplina dell’ingegneria informatica, alla fine è un modo di fare informatica. Infatti nel resto del mondo non esiste tutta questa distinzione tra informatica ed ingegnere informatico come in Italia.

Quello che critico è che l’informatica è in primis una scienza, e in quanto tale deve essere fatta in maniera formale.

Note
La scienza non è un’ingegneria, e l’ingegneria non è una scienza!

Ciò che l’informatica in quanto scienza pone nelle menti degli studenti che la studiano è il mindset agloritmico.
Ecco cosa vedo che spesso manca agli ing. ingormatici, l’approccio algoritmico!

Dato che in informatica alla fine tutto è un algoritmo (anche il computer è un algoritmo), ritengo sia inaccettabile non avere questo modo di vedere il mondo.

Invece il processo di “rendere reale” l’informatica per me è fare ingegneria.

Ritengo sia sbagliato che in Italia ci sai questa distinzione tra queste due discipline.
Il vero Informatico è sia uno scienziato che un ingegnere.
Il vero Informatico (con la I maiuscola) deve essere in grado di modellizzare un probelma in maniera formale, progettare un algoritmo che lo risolva ed analizzarne correttezza e complessità, ma deve anche essere in grado di implementarlo, in modo che tutto il lavoro formale fatto possa essere utilizzato.

Effettivamente non so quanto in realtà venga fatta bene la teoria e la pratica nelle rispettive facoltà (sicuramente varia anche da università a università), ma so che se proprio devo scegliere tra un percorso teorico ed uno pratico sceglierei quello teorico. Questo perché:

Citazione
La pratica si può acquisire con l’esperienza, la teoria è molto più complessa da acquisire senza un percorso di studi.

Infatti, nonostante la mia predisposizione per le tematiche teoriche, mi ritengo comunque abbastanza competitivo anche dal punto di vista pratico. Questo perché impiego lo stesso tempo ed interesse per studiare argomenti teorici e pratici nella mia giornata.

La differenza è che: tutte le tecnologie che conosco le ho imparate da solo, grazie anche alle solide basi teoriche che ho acquisito. Non penso però che avrei potuto imparare quanto so di teroico con le sole conoscenze pratiche…

Questa tematica è a me molto cara, e sicuramente ne parlerò meglio in un futuro blog post.