# %% RECAP # --- Albero di gioco del TRIS/Filetto # --- generazione dell'albero # --- ricerca della strategia vincente # TODO: ottimizzazione rispetto a configurazioni equivalenti ############################################################################### # %% Albero di gioco del TRIS (con 2 giocatori) # --- Configurazione: scacchiera con simboli 'o', 'x', ' ' (empty cell) # --- Convergenza: con al massimo 9 mosse la scacchiera è piena # nuovo tipo di errore per comunicare errori del gioco class FilettoError(Exception): pass class Filetto: # ADD: attributo di classe che contiene tutte le configurazioni create # NOTA: va svuotato se vogliamo lavorare su un diverso gioco (as esempio con un flag in init) istanze_create = { i : [] for i in range(10) } def __init__(self, configurazione=None, svuotare=False): "creazione di un nuovo nodo a partire da una data configurazione, rappresentata come matrice 3x3" if configurazione is None: # se non viene passata configurazione = [ [' ']*3 for i in range(3)] # ne creiamo una vuota self._configurazione = configurazione self._figli = [] # all'inizio non ci sono figli # NOTA: va svuotato se vogliamo lavorare su un diverso gioco (as esempio con un flag in init) if svuotare: self.istanze_create = { i : [] for i in range(10) } def __repr__(self): "rappresentazione sotto forma di stringa della tabella" return '\n'.join(['|'+ '|'.join(riga)+'|' for riga in self._configurazione ]) # --- Prossimo giocatore def prossimo_giocatore(self): "si inizia sempre col simbolo 'o' quindi basta contare gli ' ' per sapere a chi tocca" conteggio = self.caselle_vuote() if conteggio % 2 == 1: return 'o' elif conteggio == 0: # se non ci sono spazi return None # non è il turno di nessuno else: return 'x' # --- Configurazione vincente per un giocatore G o K (non intendo strategia) def vincente(self, giocatore): """la configurazione corrente è vincente per il giocatore se ci sono 3 dei suoi simboli in riga, colonna o diagonale""" M = self._configurazione return ( M[0][0] == M[0][1] == M[0][2] == giocatore # prima riga or M[1][0] == M[1][1] == M[1][2] == giocatore # seconda riga or M[2][0] == M[2][1] == M[2][2] == giocatore # terza riga or M[0][0] == M[1][0] == M[2][0] == giocatore # prima colonna or M[0][1] == M[1][1] == M[2][1] == giocatore # seconda colonna or M[0][2] == M[1][2] == M[2][2] == giocatore # terza colonna or M[0][0] == M[1][1] == M[2][2] == giocatore # diagonale or M[2][0] == M[1][1] == M[0][2] == giocatore # antidiagonale ) # --- Configurazione che dà patta def patta(self): "torno True se siamo in una patta (non ci sono caselle libere)" return self.prossimo_giocatore() is None # --- Mosse: inserire il simbolo di turno in una delle caselle vuote def applica_mossa(self, x, y, giocatore): """data una coordinata x,y provo a vedere se posso inserire il giocatore, e costruisco un nuovo figlio con la nuova configurazione""" # faccio controlli sul giocatore di turno if self.prossimo_giocatore() != giocatore: raise FilettoError(f"Non è il turno del giocatore {giocatore}") if self._configurazione[y][x] == ' ': # se la casella è libera copia = [ riga.copy() for riga in self._configurazione ] # copio la configurazione copia[y][x] = giocatore # e ci metto il simbolo alle coordinate x,y nuovo_figlio = Filetto(copia) # FIXME: aggiungo il figlio SOLO se NON è equivalente a qualche altro nodo # dell'albero (con lo stesso numero di spazi) # questo suggerisce di inserire in un attributo della classe tutti i nodi # creati durante questo gioco, indicizzati per numero di spazi disponibili # oppure di visitare l'albero ed esaminare tutti i nodi dello stesso livello for altra in self.istanze_create[nuovo_figlio.caselle_vuote()]: if nuovo_figlio.equivalente(altra): break else: self._figli.append(nuovo_figlio) # lo aggiungo il nuovo nodo ai figli self.istanze_create[nuovo_figlio.caselle_vuote()].append(nuovo_figlio) else: raise FilettoError(f"La casella {x} {y} è già occupata") def equivalente(self, altro): """Due posizioni sono equivalenti se uguali per rotazione o riflessione""" if not isinstance(altro, Filetto): return False if self._configurazione == altro._configurazione: return True [ [A, B, C,], [D, E, F,], [G, H, I,], ] = self._configurazione # 90+ a sinistra if [ [C, F, I,], [B, E, H,], [A, D, G,], ] == altro._configurazione: return True # 180+ a sinistra if [ [I, H, G,], [F, E, D,], [C, B, A,], ] == altro._configurazione: return True # 270+ a sinistra if [ [G, D, A,], [H, E, B,], [I, F, C,], ] == altro._configurazione: return True # riflessione orizzontale if [ [C, B, A,], [F, E, D,], [I, H, G,], ] == altro._configurazione: return True # riflessione verticale if [ [G, H, I,], [D, E, F,], [A, B, C,], ] == altro._configurazione: return True # riflessione diagonale principale if [ [A, D, G,], [B, E, H,], [C, F, I,], ] == altro._configurazione: return True # riflessione sulla diagonale secondaria if [ [I, F, C,], [H, E, B,], [G, D, A,], ] == altro._configurazione: return True # altrimenti non sono equivalenti return False def caselle_vuote(self): conteggio = 0 for riga in self._configurazione: for casella in riga: if casella == ' ': conteggio += 1 return conteggio # --- elenco delle mosse valide def mosse_valide(self): """Trovo le mosse valide (ma non ce ne sono se patta o qualcuno ha vinto)""" if self.patta(): return [] if self.vincente('x'): return [] if self.vincente('o'): return [] return [ (x,y) for y,riga in enumerate(self._configurazione) for x,casella in enumerate(riga) if casella == ' '] # --- generare l'albero di gioco def genera(self): "Se ci sono mosse valide genero i figli, quindi espando anche i figli" mosse = self.mosse_valide() pg = self.prossimo_giocatore() for x,y in mosse: self.applica_mossa(x, y, pg) for figlio in self._figli: figlio.genera() # --- Strategia vincente per il giocatore G # - esiste una mosse per G tale che per tutte le mosse di K si arriva sempre ad una posizione vincente x G # - se patta NO # - se vincente per G SI # - se vincente per K NO # - se è il turno di G basta UNA mossa che porti alla vittoria di G # - se è il turno fi K devono TUTTE portare alla vittoria di G # FIXME: controllare se la presenza/assenza di mosse equivalenti creaa problemi # - idea: sostituire il nodo equivalente con il nodo a qui equivale (DA FARE) def strategia_vincente(self, giocatore): "vedo se questa posizione ha una strategia vincente per il giocatore" if self.patta(): # no se siamo alla patta return False if self.vincente(giocatore): # sì se ho già vinto return True altro = 'o' if giocatore == 'x' else 'x' if self.vincente(altro): # no se ha vinto l'altro giocatore return False # altro modo: se non ho figli e ho vinto True else False pg = self.prossimo_giocatore() if giocatore == pg: # se tocca a me for figlio in self._figli: # e c'è almeno un figlio che è vincente posso muovermi lì e quindi questa posizione è vincente per me if figlio.strategia_vincente(giocatore): return True else: return False # altrimenti nessuno dei figli è vincente, questa posizione non è vincente else: # se invece non è il giocatore a dover giocare for figlio in self._figli: # per vincere, nessuna delle scelte dell'avversario deve essere vincente if figlio.strategia_vincente(altro): # se una lo è, questa posizione NON è vincente per me return False else: # se nessuno dei figli è vincente per l'avversario return True # allora io posso vincere da qui # %%% Ottimizzazione: Equivalenza tra configurazioni (per ridurre le dimensioni dell'albero da creare) # --- due configurazioni sono equivalenti per rotazioni, e riflessioni # - definire un modo per vedere se sono *equivalenti* # - stessa classe e configurazione == all'altra oppure a una sua rotazione/riflessione # - ricordare quali configurazioni sono state generate # - se hanno un numero di spazi diverso saranno sicuramente NON equivalenti # - quindi teniamole in gruppi separati per numero di spazi # - un attributo di classe potrebbe andare bene (ma dobbiamo ricordare di svuotarlo x nuovi giochi) # - oppure semplicemente esploriamo l'albero allo stesso livello per trovare conf equivalenti # %%% Esempi f1 = Filetto([ ['o', ' ', 'x'], [' ', ' ', 'o'], ['x', ' ', 'o'], ]) f2 = Filetto([ ['x', ' ', 'o'], [' ', ' ', 'o'], ['o', ' ', 'x'], ]) #print(f1.vincente('x')) #print(f1.prossimo_giocatore()) #print(f1.patta()) #f1.applica_mossa(1, 0, 'x') #f1.genera() #print(f1.strategia_vincente('x')) print(f1.equivalente(f2)) f1.genera() # %% semplificazione di espressioni algebriche class EspressioneError(Exception): pass # consideriamo solo espressioni algebriche con solo operatori binari (*+./^) # le foglie contengono valori (interi per semplificare) oppure nomi di variabili # senza precedenza tra operatori: ogni operazione è racchiusa tra parentesi # descrizione della sintassi # espressione ::= numero # espressione ::= variabile # espressione ::= '(' espressione operatore espressione ')' # operatore ::= '*' | '+' | '-' | '/' | '^' # variabile ::= *un carattere alfabetico* # numero ::= una sequenza di *cifre* class Espressione: def __init__(self, argomento1, operatore, argomento2): "una Espressione ha sempre due argomenti ed un operatore (+-*/^)" self._operatore = operatore self._argomento1 = argomento1 self._argomento2 = argomento2 # calcolo della espressione algebrica da un albero (con variabili) # environment è un dizionario { variabile -> valore } che permette di calcolare # l'espressione per certi valori delle variabili def calcola(self, environment): "per calcolare il valore di una espressione prima calcolo i due argomenti e poi applico l'operatore" arg1 = self._argomento1.calcola(environment) # passo l'environment alle due sottoespressioni arg2 = self._argomento2.calcola(environment) # che potrebbero contenere variabili if self._operatore == '+': return arg1 + arg2 if self._operatore == '-': return arg1 - arg2 if self._operatore == '*': return arg1 * arg2 if self._operatore == '/': return arg1 / arg2 if self._operatore == '^': return arg1 ** arg2 # + # / \ # (((2*3)+5)+(7/x)) # |----v----| larghezza espressione # 12345 # 12 # |-^-|--^--| centri delle sottoespressioni def draw(self, tartaruga): "disegno l'albero binario che corrisponde alla espressione" tartaruga.pendown() tartaruga.write(self._operatore, align='center', font=('Arial',20, 'bold')) # prima l'operatore larghezza1 = len(str(self._argomento1))*10 # larghezza del primo operando (in caratteri * 10) larghezza2 = len(str(self._argomento2))*10 # larghezza del secondo operando larghezza = len(str(self))*10 # larghezza di tutto l'albero centro = larghezza/2 # posizione della radice centro1 = larghezza1/2 # centro del sottoalbero arg1 centro2 = larghezza2/2 # centro del sottoalbero arg2 x, y = tartaruga.position() # data la posizione della radice origine = x - centro # bordo sinistro dell'albero tartaruga.pendown() tartaruga.goto(origine + centro1, y -50) # il sottoalbero SX sta centro1 dal bordo sinistro tartaruga.penup() self._argomento1.draw(tartaruga) # lo disegno tartaruga.goto(x , y) tartaruga.pendown() tartaruga.goto(origine + larghezza - centro2, y -50) # il destro sta centro2 dal bordo destro tartaruga.penup() self._argomento2.draw(tartaruga) # lo disegno tartaruga.goto(x , y) # e torno sulla radice # --- stampa della espressione algebrica da un albero def __repr__(self): # qua ci sono 2 chiamate ricorsive implicite a __repr__ per inserire gli argomenti nella stringa return f"({self._argomento1} {self._operatore} {self._argomento2})" def semplifica(self): """Semplificazioni semplici che si applicano ad un solo livello. NOTA: usano l'uguaglianza == quindi dobbiamo ridefinire __eq__ """ # prima semplifico ricorsivamente i due argomenti self._argomento1 = self._argomento1.semplifica() self._argomento2 = self._argomento2.semplifica() # poi applico le regole di semplificaazione # 1 * X = X if self._argomento1 == Numero(1) and self._operatore == '*': return self._argomento2 # 0 * X = 0 if self._argomento1 == Numero(0) and self._operatore == '*': return Numero(0) # X * 1 = X if self._argomento2 == Numero(1) and self._operatore == '*': return self._argomento1 # X * 0 = 0 if self._argomento2 == Numero(0) and self._operatore == '*': return Numero(0) # X^1 = X if self._argomento2 == Numero(1) and self._operatore == '^': return self._argomento1 # X^0 = 1 if self._argomento2 == Numero(0) and self._operatore == '^': return Numero(1) # 0 + X = X if self._argomento1 == Numero(0) and self._operatore == '+': return self._argomento2 # X + 0 = X if self._argomento2 == Numero(0) and self._operatore == '+': return self._argomento1 # X - 0 = X if self._argomento2 == Numero(0) and self._operatore == '-': return self._argomento1 # X - X = 0 if self._argomento2 == self._argomento1 and self._operatore == '-': return Numero(0) # X / 1 = X if self._argomento2 == Numero(1) and self._operatore == '/': return self._argomento1 # X / 0 = Errore if self._argomento2 == Numero(1) and self._operatore == '/': raise EspressioneError("divisione per 0") # Altre regole di semplificazione da aggiungere a piacere # E = numero *+-/^ numero = E.calcola() # (Numero * (Numero * E)) = ((Numero*Numero)*E) # (Numero + (Numero + E)) = ((Numero+Numero)*E) # ((A*B)+(A*C)) = (A*(B+C)) # ((A*B)-(A*C)) = (A*(B-C)) # (A+A) = (2*A) # (A*A) = (A^2) # (A*(A^N)) = (A^(N+1)) # ((A^N)*(A^M)) = (A^(N+M)) # altrimenti l'espressione resta invariata return self def __eq__(self, other): """Due espressioni sono uguali se: - sono entrambe espressioni - hanno lo stesso operatore - hanno gli stessi argomenti """ return (isinstance(other, Espressione) and self._operatore == other._operatore and self._argomento1 == other._argomento1 # chiamata ricorsiva implicita and self._argomento2 == other._argomento2 # chiamata ricorsiva implicita ) # N/dx = 0 # x/dx = 1 # y/dx = 0 def copia(self): "faccio ina copia ricorsiva dell'espressione" return Espressione(self._argomento1.copia(), self._operatore, self._argomento2.copia()) def deriva(self, nome_variabile): # (A+B)/dx = A/dx + B/dx # ricorsione # (A-B)/dx = A/dx - B/dx # ricorsione A = self._argomento1.copia() B = self._argomento2.copia() DA = A.deriva(nome_variabile) # .semplifica() # dopo che avrò completato le regole DB = B.deriva(nome_variabile) # .semplifica() # idem if self._operatore in ['+','-']: return Espressione(DA, self._operatore, DB) # (A*B)/dx = A*(B/dx) + B*(A/dx) # ricorsione # vale anche per A o B costante if self._operatore == '*': return Espressione(Espressione(A, '*', DB), '+', Espressione(B, '*', DA)) # (x^N)/dx = N*x^(N-1) # per semplicità evitiamo esponenti variabili if (self._operatore == '^' and type(B) == Numero and type(A) == Variabile and A._nome == nome_variabile): return Espressione( B, '*', Espressione(A, '^', Espressione(B, '-', Numero(1)))) # (A/B)/dx = (A/dx * B - B/dx * A) / B^2 # ricorsione # (1/B)/dx = - B/dx / B^2 # ricorsione if self._operatore == '/': return Espressione( Espressione( Espressione(B, '*', DA), '-', Espressione(A, '*', DB)), '/', Espressione(B, '^', Numero(2))) # TODO: funzioni di funzioni e altre regole di derivazione # (non dobbiamo tornare None) class Numero(Espressione): ''' Un numero contiene un valore numerico ''' def __init__(self, valore): self._valore = valore def copia(self): "copia di un Numero" return Numero(self._valore) # calcolo della espressione algebrica da un albero (con variabili) def calcola(self, environment): "il suo valore è proprio l'attributo self._valore" return self._valore # disegno cell'espressione con la turtle def draw(self, tartaruga): # ne stampo il valore centrato poco più in basso x,y = tartaruga.position() tartaruga.goto(x,y-30) tartaruga.pendown() tartaruga.write(self._valore, align='center', font=('Arial',20,'bold')) tartaruga.penup() tartaruga.goto(x,y) # --- stampa della espressione algebrica da un albero def __repr__(self): "il testo che rappresenta questo oggetto non è altro che il valore come stringa" return str(self._valore) # FIXME: se si tratta di valori float potremmo indicare entro quale precisione devono essere uguali def __eq__(self, other): "un Numero è uguale ad un altro se ha lo stesso valore" return ( isinstance(other, Numero) and self._valore == other._valore ) # non c'è nulla da semplificare, torno self def semplifica(self): return self # torno sempre 0 def deriva(self, nome_variabile): return Numero(0) class Variabile(Espressione): def __init__(self, nome): "una variabile ha un nome (stringa)" self._nome = nome def copia(self): return Variabile(self._nome) # calcolo del valore della variabile def calcola(self, environment): "dato un dizionario { nome -> valore }, una variabile ha il valore che nel dizionario corrisponde al suo nome" try: return environment[self._nome] except KeyError: raise EspressioneError(f"La variabile {self._nome} non è presente nell'environment") def draw(self, tartaruga): "disegno il nome della variabile centrato" x,y = tartaruga.position() tartaruga.goto(x,y-30) tartaruga.pendown() tartaruga.write(self._nome, align='center', font=('Arial',20,'bold')) tartaruga.penup() tartaruga.goto(x,y) # --- stampa della espressione algebrica da un albero def __repr__(self): "come stringa la variabile è rappresentata dal suo nome" return self._nome # una Variabile è uguale ad un'altra se ha lo stesso nome def __eq__(self, other): return ( isinstance(other, Variabile) and self._nome == other._nome ) # non c'è nulla da semplificare, torno self def semplifica(self): return self # torno 1 o 0 a seconda se il nome è uguale def deriva(self, nome_variabile): if self._nome == nome_variabile: return Numero(1) else: return Numero(0) # %%% costruzione dell'albero da una espressione algebrica (parser) # la funzione analizza la parte iniziale della stringa, riconoscendo l'espressione # e la torna assieme alla parte di testo che ancoraa non è stata esaminata def analizza(stringa): # FIXME: Prima di chiamare analizza conviene togliere eventuali spazi con replace primo, *resto = stringa # prendo il primo carattere e lascio in resto i rimanenti if primo.isdecimal(): # se è una cifra # torno un Numero con tutte le cifre che trovo valore = primo # concateno le cifre while resto and resto[0].isdecimal(): # se ce ne sono ancora valore += resto.pop(0) # tolgo la prima cifra # quando non ne trovo più return Numero(int(valore)), resto # costruisco il numero e torno il resto del testo non analizzato if primo == '(': # se invece il primo carattere è una '(' devo riconoscere una espressione # torno una espressione cercando: espressione operatore espressione ')' arg1, resto1 = analizza(resto) # con una chiamata ricorsiva riconosco la prima espressione operatore, *resto2 = resto1 # nel resto del testo il primo carattere è l'operatore if operatore not in '*+-/^': # se è sbagliato lancio un errore raise EspressioneError("mi aspettavo un operatore '*+-/^' invece di "+operatore) arg2, resto3 = analizza(resto2) # analizzo il testo dopo l'operatore if resto3[0] != ')': # e subito dopo mi aspetto di trovare la ')' raise EspressioneError("mi aspettavo una ) e invece ho trovato una "+resto3[0]) # se tutto è andato bene posso costruire l'espressione e tornare il testo che segue la ')' return Espressione(arg1, operatore, arg2), resto3[1:] else: # altrimenti dovrebbe essere una variabile (con un solo carattere) # torno una Variabile return Variabile(primo), resto # la costruisco e torno il resto dei caratteri che la seguono # %%% Esempi E, resto = analizza('(45*(1*(y+((52*x)^3))))') env = {'x': 2, 'y': 3, 'z': 4} print(E) #print(E.semplifica()) print(E.deriva('x')) # %%% visualizzazione dell'abero con la turtle import turtle t = turtle.Turtle() t.reset() #E.draw(t) E1,_ = analizza("((((3+y)-12345678)+y)*12345678)") E1.draw(t) # %%% semplificazione # - equivalenza (e operatori commutativi) # - assorbimento su * + - / # - esiste una forma "canonica" della formula? (aiuta nei controlli e diminuisce il n° di regole) # - singolo passaggio di semplificazione # - applicazione ricorsiva della semplificazione partendo dalle foglie a salire # %% derivazione di espressioni algebriche # - regole di derivazione come trasformazioni della espressione # - che applicano ricorsivamente la derivazione alle diverse sottoespressioni se necessario # Esempi: # N/dx = 0 # x/dx = 1 # y/dx = 0 # (A+B)/dx = A/dx + B/dx # ricorsione # (A-B)/dx = A/dx - B/dx # ricorsione # (A*B)/dx = A*(B/dx) - B*(A/dx) # ricorsione # vale anche per A o B costante # (x^N)/dx = N*x^(N-1) # per semplicità evitiamo esponenti variabili # (A/B)/dx = (A/dx * B - B/dx * A) / B^2 # ricorsione # (1/B)/dx = - B/dx / B^2 # ricorsione # %%% Rotazione di una immagine quadrata di lato 2^n ricorsivamente # 1 2 # 3 4 # 2 4 # 1 3 # 0 1 2 3 # 4 5 6 7 # 8 9 A B # C D E F # 3 7 B F # 2 6 A E # 1 5 9 D # 0 4 8 C # caso base: N==1 la matrice resta così # se N>1: # divido in 4 sottomatrici # le ruoto di 90° (chiamata ricorsiva) # fondo in una matrice più grande def dividi(matrice): "divido la matrice nei suoi 4 quadranti" # NOTA: la matrice deve avere dimensione >=2 N = len(matrice) assert N >= 2, "non posso dividere una matrice di lato 1 o 0" middle = N//2 A = [ riga[:middle] for riga in matrice[:middle] ] B = [ riga[middle:] for riga in matrice[:middle] ] C = [ riga[:middle] for riga in matrice[middle:] ] D = [ riga[middle:] for riga in matrice[middle:] ] return A, B, C, D def fondi(A, B, C, D): "fondo 4 matrici in una sola" AB = [ rA+rB for rA,rB in zip(A,B) ] CD = [ rC+rD for rC,rD in zip(C,D) ] return AB + CD # A B # C D # B D # A C def ruotaP2(M): "ruoto una matrice che ha lato 2^L" N = len(M) if N == 1: return M else: # divido in 4 quadranti A, B, C, D = dividi(M) # li ruoto AR = ruotaP2(A) BR = ruotaP2(B) CR = ruotaP2(C) DR = ruotaP2(D) # li riunisco ruotandone la posizione return fondi(BR, DR, AR, CR) def ruota(M): "generico ruota applicabile a matrici != da potenza di 2" # allarga la matrice alla prossima potenza di 2 W = len(M[0]) H = len(M) # trovo la prossima potenza di 2 maggiore o uguale a W potenzaW = 0 while 2**potenzaW < W: potenzaW += 1 # trovo la prossima potenza di 2 maggiore o uguale a H potenzaH = 0 while 2**potenzaH < H: potenzaH += 1 DW = 2**potenzaW - W # colonne da aggiungere DH = 2**potenzaH - H # righe da aggiungere for riga in M: # aggiungo DW colonne riga += [(0,255,0)]*DW for _ in range(DH): # aggiungo DH righe M += [[(0,255,0)] * (2**potenzaW)] M1 = ruotaP2(M) # ruoto l'immagine M1[:DW] = [] # tolgo le prime DW righe for riga in M1: # tolgo le ultime DH colonne riga[-DH:] = [] return M1 # %%% Esempi # 3 7 B F # 2 6 A E # 1 5 9 D # 0 4 8 C M = [ [0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 'A', 'B'], ['C', 'D', 'E', 'F']] N = [ [1, 2], [3,4] ] print(*dividi(M), sep='\n\n') print(fondi(*dividi(M))) print(*ruotaP2(M), sep='\n') # %%% Rotazione di immagini import images bianca = [ [(255,255,255)]*(2**8) for i in range(2**8) ] #images.save(bianca, 'scarabocchio.png') img = images.load('scarabocchio.png') ruotata = ruota(img) images.save(ruotata, 'scarabocchio-90°.png') # %%% scara = images.load('scarabocchio.png') bordata = ruota(scara) images.save(bordata, 'scara-2.png') # %% BYE BYE