neuralt-work 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aquilesinho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: neuralt-work
3
+ Version: 0.1.0
4
+ Summary: A lightweight neural-network learning library for Python.
5
+ Author: Aquilesinho
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Aquilesinho
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Classifier: Development Status :: 3 - Alpha
29
+ Classifier: Intended Audience :: Developers
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: Programming Language :: Python :: 3 :: Only
32
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
33
+ Requires-Python: >=3.9
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ Requires-Dist: numpy>=1.24
37
+ Dynamic: license-file
38
+
39
+ # NeuraltWork
40
+
41
+ NeuraltWork is a Python neural-network learning library.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install neuralt-work
47
+ ```
48
+
49
+ ## Import
50
+
51
+ ```python
52
+ import neuraltwork as nn
53
+ ```
54
+
55
+ ## Status
56
+
57
+ This project is in early development. Its API and behavior may change between releases.
58
+
59
+ ## License
60
+
61
+ This project is distributed under the MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,23 @@
1
+ # NeuraltWork
2
+
3
+ NeuraltWork is a Python neural-network learning library.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install neuralt-work
9
+ ```
10
+
11
+ ## Import
12
+
13
+ ```python
14
+ import neuraltwork as nn
15
+ ```
16
+
17
+ ## Status
18
+
19
+ This project is in early development. Its API and behavior may change between releases.
20
+
21
+ ## License
22
+
23
+ This project is distributed under the MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "neuralt-work"
7
+ version = "0.1.0"
8
+ description = "A lightweight neural-network learning library for Python."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { file = "LICENSE" }
12
+ authors = [
13
+ { name = "Aquilesinho" }
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
21
+ ]
22
+ dependencies = [
23
+ "numpy>=1.24"
24
+ ]
25
+
26
+ [tool.setuptools]
27
+ package-dir = { "" = "src" }
28
+
29
+ [tool.setuptools.packages.find]
30
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: neuralt-work
3
+ Version: 0.1.0
4
+ Summary: A lightweight neural-network learning library for Python.
5
+ Author: Aquilesinho
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Aquilesinho
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Classifier: Development Status :: 3 - Alpha
29
+ Classifier: Intended Audience :: Developers
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: Programming Language :: Python :: 3 :: Only
32
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
33
+ Requires-Python: >=3.9
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ Requires-Dist: numpy>=1.24
37
+ Dynamic: license-file
38
+
39
+ # NeuraltWork
40
+
41
+ NeuraltWork is a Python neural-network learning library.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install neuralt-work
47
+ ```
48
+
49
+ ## Import
50
+
51
+ ```python
52
+ import neuraltwork as nn
53
+ ```
54
+
55
+ ## Status
56
+
57
+ This project is in early development. Its API and behavior may change between releases.
58
+
59
+ ## License
60
+
61
+ This project is distributed under the MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/neuralt_work.egg-info/PKG-INFO
5
+ src/neuralt_work.egg-info/SOURCES.txt
6
+ src/neuralt_work.egg-info/dependency_links.txt
7
+ src/neuralt_work.egg-info/requires.txt
8
+ src/neuralt_work.egg-info/top_level.txt
9
+ src/neuraltwork/__init__.py
10
+ src/neuraltwork/main.py
@@ -0,0 +1 @@
1
+ numpy>=1.24
@@ -0,0 +1 @@
1
+ neuraltwork
@@ -0,0 +1 @@
1
+ from .main import *
@@ -0,0 +1,717 @@
1
+ import os
2
+ os.environ["OPENBLAS_NUM_THREADS"] = "1"
3
+ os.environ["OMP_NUM_THREADS"] = "1"
4
+ os.environ["MKL_NUM_THREADS"] = "1"
5
+ import pickle
6
+ import random
7
+ import time
8
+ from collections import deque
9
+ import numpy as np
10
+
11
+
12
+ ACAO_ESPERAR = 0
13
+ ACAO_PRIMEIRA_FUNCAO = 1
14
+
15
+ _ENTRADAS_BASE = 4
16
+ _MAX_MEMORIA = 50000
17
+ _LOTE = 32
18
+ _FREQUENCIA_APRENDIZADO = 64
19
+ _FREQUENCIA_DECISAO = 4
20
+
21
+
22
+ class _RedeNeural:
23
+ def __init__(self, entradas=_ENTRADAS_BASE, ocultas1=64, ocultas2=64, saidas=2):
24
+ self.W1 = np.random.randn(entradas, ocultas1).astype(np.float32) * np.sqrt(2.0 / entradas)
25
+ self.b1 = np.zeros(ocultas1, dtype=np.float32)
26
+ self.W2 = np.random.randn(ocultas1, ocultas2).astype(np.float32) * np.sqrt(2.0 / ocultas1)
27
+ self.b2 = np.zeros(ocultas2, dtype=np.float32)
28
+ self.W3 = np.random.randn(ocultas2, saidas).astype(np.float32) * np.sqrt(2.0 / ocultas2)
29
+ self.b3 = np.zeros(saidas, dtype=np.float32)
30
+ self.m = [np.zeros_like(p) for p in self.parametros()]
31
+ self.v = [np.zeros_like(p) for p in self.parametros()]
32
+ self.passos_adam = 0
33
+
34
+ def parametros(self):
35
+ return [self.W1, self.b1, self.W2, self.b2, self.W3, self.b3]
36
+
37
+ def prever(self, estados):
38
+ estados = np.asarray(estados, dtype=np.float32)
39
+ a1 = np.maximum(0, estados @ self.W1 + self.b1)
40
+ a2 = np.maximum(0, a1 @ self.W2 + self.b2)
41
+ return a2 @ self.W3 + self.b3
42
+
43
+ def treinar(self, estados, acoes, alvos):
44
+ estados = np.asarray(estados, dtype=np.float32)
45
+ acoes = np.asarray(acoes, dtype=np.int64)
46
+ alvos = np.asarray(alvos, dtype=np.float32)
47
+
48
+ z1 = estados @ self.W1 + self.b1
49
+ a1 = np.maximum(0, z1)
50
+ z2 = a1 @ self.W2 + self.b2
51
+ a2 = np.maximum(0, z2)
52
+ saida = a2 @ self.W3 + self.b3
53
+
54
+ quantidade = len(estados)
55
+ delta3 = np.zeros_like(saida)
56
+ delta3[np.arange(quantidade), acoes] = (
57
+ 2.0 * (saida[np.arange(quantidade), acoes] - alvos) / quantidade
58
+ )
59
+
60
+ grad_W3 = a2.T @ delta3
61
+ grad_b3 = np.sum(delta3, axis=0)
62
+ delta2 = (delta3 @ self.W3.T) * (z2 > 0)
63
+ grad_W2 = a1.T @ delta2
64
+ grad_b2 = np.sum(delta2, axis=0)
65
+ delta1 = (delta2 @ self.W2.T) * (z1 > 0)
66
+ grad_W1 = estados.T @ delta1
67
+ grad_b1 = np.sum(delta1, axis=0)
68
+
69
+ gradientes = [grad_W1, grad_b1, grad_W2, grad_b2, grad_W3, grad_b3]
70
+ self.passos_adam += 1
71
+ beta1 = 0.9
72
+ beta2 = 0.999
73
+ taxa = 0.0005
74
+ epsilon = 1e-8
75
+
76
+ for i, (parametro, gradiente) in enumerate(zip(self.parametros(), gradientes)):
77
+ self.m[i] = beta1 * self.m[i] + (1.0 - beta1) * gradiente
78
+ self.v[i] = beta2 * self.v[i] + (1.0 - beta2) * (gradiente ** 2)
79
+ m_corrigido = self.m[i] / (1.0 - beta1 ** self.passos_adam)
80
+ v_corrigido = self.v[i] / (1.0 - beta2 ** self.passos_adam)
81
+ parametro -= taxa * m_corrigido / (np.sqrt(v_corrigido) + epsilon)
82
+
83
+ def copiar_pesos(self, outra):
84
+ for destino, origem in zip(self.parametros(), outra.parametros()):
85
+ destino[:] = origem
86
+
87
+
88
+ class _AgenteDQN:
89
+ def __init__(self, quantidade_acoes, caminho, entradas):
90
+ self.rede = _RedeNeural(entradas=entradas, saidas=quantidade_acoes)
91
+ self.rede_alvo = _RedeNeural(entradas=entradas, saidas=quantidade_acoes)
92
+ self.rede_alvo.copiar_pesos(self.rede)
93
+ self.memoria = deque(maxlen=_MAX_MEMORIA)
94
+ self.memoria_melhor = deque(maxlen=_MAX_MEMORIA)
95
+ self.experiencias_episodio = deque(maxlen=_MAX_MEMORIA)
96
+ self.gamma = 0.99
97
+ self.epsilon = 1.0
98
+ self.epsilon_minimo = 0.05
99
+ self.epsilon_decay = 0.9995
100
+ self.lote = _LOTE
101
+ self.passos = 0
102
+ self.caminho = caminho
103
+ self.carregar()
104
+
105
+ def escolher_acao(self, estado):
106
+ if random.random() < self.epsilon:
107
+ return random.randrange(self.rede.W3.shape[1])
108
+ return int(np.argmax(self.rede.prever([estado])[0]))
109
+
110
+ def guardar(self, estado, acao, recompensa, proximo, terminou):
111
+ experiencia = (
112
+ np.asarray(estado, dtype=np.float32),
113
+ int(acao),
114
+ float(recompensa),
115
+ np.asarray(proximo, dtype=np.float32),
116
+ bool(terminou)
117
+ )
118
+ self.memoria.append(experiencia)
119
+ self.experiencias_episodio.append(experiencia)
120
+
121
+ def aprender(self):
122
+ total = len(self.memoria) + len(self.memoria_melhor)
123
+ if total < self.lote:
124
+ return
125
+
126
+ qtd_atual = min(16, len(self.memoria))
127
+ qtd_melhor = min(16, len(self.memoria_melhor))
128
+ faltam = self.lote - qtd_atual - qtd_melhor
129
+
130
+ if faltam > 0:
131
+ extra = min(faltam, len(self.memoria) - qtd_atual)
132
+ qtd_atual += extra
133
+ faltam -= extra
134
+ if faltam > 0:
135
+ qtd_melhor += min(faltam, len(self.memoria_melhor) - qtd_melhor)
136
+
137
+ amostra = []
138
+ if qtd_atual:
139
+ amostra.extend(random.sample(list(self.memoria), qtd_atual))
140
+ if qtd_melhor:
141
+ amostra.extend(random.sample(list(self.memoria_melhor), qtd_melhor))
142
+ if len(amostra) < self.lote:
143
+ return
144
+
145
+ estados = np.asarray([item[0] for item in amostra], dtype=np.float32)
146
+ acoes = np.asarray([item[1] for item in amostra], dtype=np.int64)
147
+ recompensas = np.asarray([item[2] for item in amostra], dtype=np.float32)
148
+ proximos = np.asarray([item[3] for item in amostra], dtype=np.float32)
149
+ terminou = np.asarray([item[4] for item in amostra], dtype=np.float32)
150
+
151
+ futuros = np.max(self.rede_alvo.prever(proximos), axis=1)
152
+ alvos = recompensas + self.gamma * futuros * (1.0 - terminou)
153
+ self.rede.treinar(estados, acoes, alvos)
154
+
155
+ self.passos += 1
156
+ if self.passos % 500 == 0:
157
+ self.rede_alvo.copiar_pesos(self.rede)
158
+
159
+ self.epsilon = max(self.epsilon_minimo, self.epsilon * self.epsilon_decay)
160
+
161
+ def encerrar_episodio(self):
162
+ if self.experiencias_episodio:
163
+ self.memoria_melhor = deque(self.experiencias_episodio, maxlen=_MAX_MEMORIA)
164
+ self.experiencias_episodio = deque(maxlen=_MAX_MEMORIA)
165
+
166
+ def salvar(self):
167
+ dados = {
168
+ "versao": 3,
169
+ "acoes": self.rede.W3.shape[1],
170
+ "entradas": self.rede.W1.shape[0],
171
+ "pesos_rede": [p.copy() for p in self.rede.parametros()],
172
+ "pesos_alvo": [p.copy() for p in self.rede_alvo.parametros()],
173
+ "memoria": list(self.memoria),
174
+ "memoria_melhor": list(self.memoria_melhor),
175
+ "epsilon": self.epsilon,
176
+ "passos": self.passos,
177
+ "adam_m": [v.copy() for v in self.rede.m],
178
+ "adam_v": [v.copy() for v in self.rede.v],
179
+ "adam_passos": self.rede.passos_adam
180
+ }
181
+ temporario = self.caminho + "." + str(os.getpid()) + ".tmp"
182
+ try:
183
+ with open(temporario, "wb") as arquivo:
184
+ pickle.dump(dados, arquivo, protocol=pickle.HIGHEST_PROTOCOL)
185
+ for tentativa in range(5):
186
+ try:
187
+ os.replace(temporario, self.caminho)
188
+ return
189
+ except PermissionError:
190
+ if tentativa == 4:
191
+ raise
192
+ time.sleep(0.3)
193
+ except Exception as erro:
194
+ print("neuralnetwork: erro ao salvar:", erro)
195
+ finally:
196
+ if os.path.exists(temporario):
197
+ try:
198
+ os.remove(temporario)
199
+ except OSError:
200
+ pass
201
+
202
+ def carregar(self):
203
+ if not os.path.exists(self.caminho):
204
+ return
205
+ try:
206
+ with open(self.caminho, "rb") as arquivo:
207
+ dados = pickle.load(arquivo)
208
+ if (dados.get("versao") != 3 or
209
+ dados.get("acoes") != self.rede.W3.shape[1] or
210
+ dados.get("entradas") != self.rede.W1.shape[0]):
211
+ print("neuralnetwork: memória incompatível; iniciando do zero.")
212
+ return
213
+ for atual, salvo in zip(self.rede.parametros(), dados["pesos_rede"]):
214
+ if atual.shape == salvo.shape:
215
+ atual[:] = salvo
216
+ for atual, salvo in zip(self.rede_alvo.parametros(), dados["pesos_alvo"]):
217
+ if atual.shape == salvo.shape:
218
+ atual[:] = salvo
219
+ self.memoria.extend(dados.get("memoria", []))
220
+ self.memoria_melhor.extend(dados.get("memoria_melhor", []))
221
+ self.epsilon = float(dados.get("epsilon", 1.0))
222
+ self.passos = int(dados.get("passos", 0))
223
+ self.rede.m = [v.copy() for v in dados.get("adam_m", self.rede.m)]
224
+ self.rede.v = [v.copy() for v in dados.get("adam_v", self.rede.v)]
225
+ self.rede.passos_adam = int(dados.get("adam_passos", 0))
226
+ print("neuralnetwork: memória carregada.")
227
+ except Exception as erro:
228
+ print("neuralnetwork: não foi possível carregar a memória:", erro)
229
+
230
+
231
+ _player_pos = None
232
+ _player_size = None
233
+ _player_velocity_y = 0.0
234
+ _obstaculos = []
235
+ _obsnot = []
236
+ _paredes = []
237
+ _quantidades_observacao = None
238
+ _funcoes = []
239
+ _recompensa_atual = 1.0
240
+ _estado_anterior = None
241
+ _acao_anterior = None
242
+ _contador_atualizacoes = 0
243
+ _agente = None
244
+ _total_tentativas = 0
245
+ _melhor_recompensa = 0.0
246
+ _pasta = os.path.dirname(os.path.abspath(__file__))
247
+ _caminho_memoria = os.path.join(_pasta, "neuralnetwork_memory.pkl")
248
+
249
+
250
+ def player(posicao, tamanho, velocity_y=0.0):
251
+ """Atualiza a posição e o tamanho do jogador. Chame a cada frame."""
252
+ global _player_pos, _player_size, _player_velocity_y
253
+ _player_pos = tuple(posicao)
254
+ _player_size = tuple(tamanho)
255
+ _player_velocity_y = float(velocity_y)
256
+
257
+
258
+ def _validar_hitboxes(lista, nome):
259
+ """Valida listas no formato [((x, y), (largura, altura)), ...]."""
260
+ if not isinstance(lista, (list, tuple)):
261
+ raise TypeError("nn." + nome + " espera uma lista ou tupla.")
262
+
263
+ resultado = []
264
+ for item in lista:
265
+ try:
266
+ posicao, tamanho = item
267
+ if len(posicao) != 2 or len(tamanho) != 2:
268
+ raise ValueError
269
+ x = float(posicao[0])
270
+ y = float(posicao[1])
271
+ largura = float(tamanho[0])
272
+ altura = float(tamanho[1])
273
+ if largura < 0 or altura < 0:
274
+ raise ValueError
275
+ except (TypeError, ValueError, IndexError):
276
+ raise ValueError(
277
+ "Item inválido em nn." + nome + ". Use [((x, y), (largura, altura)), ...]."
278
+ )
279
+ resultado.append((x, y, largura, altura))
280
+ return resultado
281
+
282
+
283
+ def obstacles(lista):
284
+ """Define TODOS os obstáculos que podem matar o jogador."""
285
+ global _obstaculos
286
+ _obstaculos = sorted(_validar_hitboxes(lista, "obstacles"), key=lambda item: item[0])
287
+
288
+
289
+ def obsnot(lista):
290
+ """Define objetos de efeito incerto; a IA aprende pelo resultado das tentativas."""
291
+ global _obsnot
292
+ _obsnot = sorted(_validar_hitboxes(lista, "obsnot"), key=lambda item: item[0])
293
+
294
+
295
+ def wall(lista):
296
+ """Define hitboxes de paredes/limites que não matam o jogador."""
297
+ global _paredes
298
+ _paredes = sorted(_validar_hitboxes(lista, "wall"), key=lambda item: item[0])
299
+
300
+
301
+ def functions(lista):
302
+ """Define ações. A ação 0 é sempre esperar; lista[0] vira ação 1."""
303
+ global _funcoes, _agente, _estado_anterior, _acao_anterior
304
+ if not isinstance(lista, (list, tuple)):
305
+ raise TypeError("nn.functions espera uma lista ou tupla de funções.")
306
+ if not lista:
307
+ raise ValueError("nn.functions precisa de pelo menos uma função.")
308
+ if not all(callable(funcao) for funcao in lista):
309
+ raise TypeError("Todos os itens de nn.functions precisam ser funções.")
310
+ mudou = len(_funcoes) != len(lista)
311
+ _funcoes = list(lista)
312
+ if mudou:
313
+ _agente = None
314
+ _estado_anterior = None
315
+ _acao_anterior = None
316
+
317
+
318
+ def reward(pontos):
319
+ """Define a recompensa do passo atual. Ex.: nn.reward(points)."""
320
+ global _recompensa_atual
321
+ _recompensa_atual = float(pontos)
322
+
323
+
324
+ def _obter_estado():
325
+ global _quantidades_observacao
326
+
327
+ if _player_pos is None or _player_size is None:
328
+ raise RuntimeError("Chame nn.player(posicao, tamanho) antes de nn.update().")
329
+
330
+ px, py = _player_pos
331
+ largura, altura = _player_size
332
+ quantidades = (len(_obstaculos), len(_obsnot), len(_paredes))
333
+
334
+ # A quantidade de hitboxes precisa permanecer estável para manter
335
+ # o mesmo tamanho de entrada da rede durante o treinamento.
336
+ if _quantidades_observacao is None:
337
+ _quantidades_observacao = quantidades
338
+ elif quantidades != _quantidades_observacao:
339
+ raise RuntimeError(
340
+ "A quantidade de hitboxes mudou depois do início da IA. "
341
+ "Passe todos os itens de cada categoria em todos os frames, "
342
+ "mesmo os que estão longe da tela."
343
+ )
344
+
345
+ estado = [
346
+ max(-1.0, min(1.0, py / 600.0)),
347
+ max(-1.0, min(1.0, _player_velocity_y / 900.0)),
348
+ max(0.0, min(1.0, largura / 300.0)),
349
+ max(0.0, min(1.0, altura / 300.0))
350
+ ]
351
+
352
+ # Cada hitbox é incluída. A ordem por X deixa a observação consistente.
353
+ # As três categorias ficam separadas para a rede aprender suas diferenças.
354
+ for grupo in (_obstaculos, _obsnot, _paredes):
355
+ for x, y, w, h in grupo:
356
+ estado.extend([
357
+ max(-10.0, min(10.0, (x - px) / 1050.0)),
358
+ max(-10.0, min(10.0, (y - py) / 600.0)),
359
+ max(0.0, min(10.0, w / 300.0)),
360
+ max(0.0, min(10.0, h / 300.0))
361
+ ])
362
+
363
+ return np.asarray(estado, dtype=np.float32)
364
+
365
+
366
+ def _garantir_agente(entradas):
367
+ global _agente
368
+ if not _funcoes:
369
+ raise RuntimeError("Configure as ações com nn.functions([...]) antes de nn.update().")
370
+ if _agente is None:
371
+ _agente = _AgenteDQN(len(_funcoes) + 1, _caminho_memoria, entradas)
372
+
373
+
374
+ def update():
375
+ """Executa uma decisão da IA. Chame uma vez por passo de jogo/física.
376
+
377
+ Retorna True após executar a decisão da IA.
378
+ """
379
+ global _estado_anterior, _acao_anterior, _recompensa_atual
380
+ global _contador_atualizacoes
381
+ if _player_pos is None or _player_size is None:
382
+ raise RuntimeError("Chame nn.player(posicao, tamanho) antes de nn.update().")
383
+
384
+ estado = _obter_estado()
385
+ _garantir_agente(len(estado))
386
+ _contador_atualizacoes += 1
387
+ if _estado_anterior is not None and _acao_anterior is not None:
388
+ _agente.guardar(_estado_anterior, _acao_anterior, _recompensa_atual, estado, False)
389
+ if _contador_atualizacoes % _FREQUENCIA_APRENDIZADO == 0:
390
+ _agente.aprender()
391
+
392
+ acao = _agente.escolher_acao(estado)
393
+ if acao > 0:
394
+ _funcoes[acao - 1]()
395
+
396
+ _estado_anterior = estado
397
+ _acao_anterior = acao
398
+ _recompensa_atual = 1.0
399
+ return True
400
+
401
+
402
+ def reset():
403
+ """Encerra a tentativa atual; preserva os pesos e o aprendizado."""
404
+ global _estado_anterior, _acao_anterior, _recompensa_atual
405
+ global _total_tentativas, _melhor_recompensa
406
+
407
+ if _agente is not None and _estado_anterior is not None and _acao_anterior is not None:
408
+ proximo = _obter_estado() if _player_pos is not None else _estado_anterior
409
+ recompensa_final = _recompensa_atual
410
+ _agente.guardar(_estado_anterior, _acao_anterior, recompensa_final, proximo, True)
411
+ _agente.aprender()
412
+ if _agente.experiencias_episodio:
413
+ soma = sum(item[2] for item in _agente.experiencias_episodio)
414
+ if soma > _melhor_recompensa:
415
+ _melhor_recompensa = soma
416
+ _agente.encerrar_episodio()
417
+ else:
418
+ _agente.experiencias_episodio = deque(maxlen=_MAX_MEMORIA)
419
+ _total_tentativas += 1
420
+ if _total_tentativas % 10 == 0:
421
+ _agente.salvar()
422
+
423
+ _estado_anterior = None
424
+ _acao_anterior = None
425
+ _recompensa_atual = 1.0
426
+
427
+
428
+ def save():
429
+ """Salva a rede e as memórias manualmente."""
430
+ if _agente is not None:
431
+ _agente.salvar()
432
+
433
+
434
+ def stats():
435
+ """Retorna informações úteis: epsilon, tentativas e tamanhos das memórias."""
436
+ if _agente is None:
437
+ return {
438
+ "epsilon": 1.0,
439
+ "tentativas": _total_tentativas,
440
+ "memoria": 0,
441
+ "memoria_melhor": 0
442
+ }
443
+ return {
444
+ "epsilon": _agente.epsilon,
445
+ "tentativas": _total_tentativas,
446
+ "memoria": len(_agente.memoria),
447
+ "memoria_melhor": len(_agente.memoria_melhor)
448
+ }
449
+
450
+ # ================= MODO POPULACAO ================= #
451
+ # nn.players([...]) ativa o modo de varias IAs. Cada item:
452
+ # (posicao, tamanho, velocidade_y, viva)
453
+ _QUANTIDADE_JOGADORES = 100
454
+ _modo_populacao = False
455
+ _jogadores = []
456
+ _agentes_populacao = []
457
+ _recompensas_populacao = []
458
+ _pontuacoes_populacao = []
459
+ _mortos_anteriores = []
460
+ _ticks_populacao = 0
461
+ _recompensas_acumuladas = [0.0] * 100
462
+ _geracao = 1
463
+ _melhor_indice = 0
464
+ _melhor_pontuacao_geral = float("-inf")
465
+ _caminho_melhor_populacao = os.path.join(_pasta, "neuralnetwork_best_population.pkl")
466
+
467
+
468
+ def _novo_agente_populacao(indice, copiar_de=None):
469
+ caminho = os.path.join(_pasta, "population_agent_" + str(indice) + ".pkl")
470
+ agente = _AgenteDQN(len(_funcoes) + 1, caminho, _entradas_populacao)
471
+ if copiar_de is not None:
472
+ # Na primeira geracao, copiar_de pode ser uma _RedeNeural
473
+ # carregada do arquivo da melhor populacao. Nas seguintes,
474
+ # pode ser um _AgenteDQN completo.
475
+ if isinstance(copiar_de, _AgenteDQN):
476
+ rede_origem = copiar_de.rede
477
+ epsilon_origem = copiar_de.epsilon
478
+ else:
479
+ rede_origem = copiar_de
480
+ epsilon_origem = 1.0
481
+
482
+ agente.rede.copiar_pesos(rede_origem)
483
+ agente.rede_alvo.copiar_pesos(rede_origem)
484
+ agente.epsilon = max(0.25, epsilon_origem)
485
+ agente.memoria.clear()
486
+ agente.memoria_melhor.clear()
487
+ agente.experiencias_episodio.clear()
488
+ return agente
489
+
490
+
491
+ def players(lista):
492
+ """Atualiza os 100 jogadores: [(pos, tamanho, velocidade_y, viva), ...]."""
493
+ global _modo_populacao, _jogadores, _agentes_populacao
494
+ global _recompensas_populacao, _pontuacoes_populacao, _mortos_anteriores
495
+ global _entradas_populacao
496
+ if not isinstance(lista, (list, tuple)) or not lista:
497
+ raise ValueError("nn.players espera uma lista de jogadores.")
498
+ if len(lista) != _QUANTIDADE_JOGADORES:
499
+ raise ValueError("nn.players precisa receber exatamente 100 jogadores.")
500
+ normalizados = []
501
+ for item in lista:
502
+ if len(item) == 3:
503
+ pos, tamanho, velocidade = item
504
+ viva = True
505
+ elif len(item) == 4:
506
+ pos, tamanho, velocidade, viva = item
507
+ else:
508
+ raise ValueError("Cada jogador deve ser (pos, tamanho, velocidade, viva).")
509
+ normalizados.append((tuple(pos), tuple(tamanho), float(velocidade), bool(viva)))
510
+ _modo_populacao = True
511
+ _jogadores = normalizados
512
+ if not _agentes_populacao:
513
+ # A dimensão é calculada com a mesma observação usada no modo individual.
514
+ global _player_pos, _player_size, _player_velocity_y
515
+ _player_pos, _player_size, _player_velocity_y = normalizados[0][:3]
516
+ estado = _obter_estado()
517
+ _entradas_populacao = len(estado)
518
+ melhor = None
519
+ if os.path.exists(_caminho_melhor_populacao):
520
+ try:
521
+ with open(_caminho_melhor_populacao, "rb") as arquivo:
522
+ dados = pickle.load(arquivo)
523
+ melhor = _RedeNeural(entradas=_entradas_populacao, saidas=len(_funcoes) + 1)
524
+ for atual, salvo in zip(melhor.parametros(), dados["pesos"]):
525
+ if atual.shape == salvo.shape:
526
+ atual[:] = salvo
527
+ except Exception:
528
+ melhor = None
529
+ _agentes_populacao = [_novo_agente_populacao(i, melhor) for i in range(100)]
530
+ _recompensas_populacao = [0.0] * 100
531
+ _pontuacoes_populacao = [0.0] * 100
532
+ _mortos_anteriores = [False] * 100
533
+
534
+
535
+ def reward(pontos):
536
+ """No modo players, recebe uma lista de 100 recompensas individuais."""
537
+ global _recompensa_atual, _recompensas_populacao
538
+ if _modo_populacao:
539
+ if not isinstance(pontos, (list, tuple)) or len(pontos) != 100:
540
+ raise ValueError("No modo nn.players, nn.reward precisa receber 100 valores.")
541
+ _recompensas_populacao = [float(v) for v in pontos]
542
+ else:
543
+ _recompensa_atual = float(pontos)
544
+
545
+
546
+ def _processar_jogador(argumentos):
547
+ """Processa uma IA sequencialmente, sem criar threads concorrentes."""
548
+ i, estado = argumentos
549
+ jogador = _jogadores[i]
550
+ agente = _agentes_populacao[i]
551
+ recompensa = _recompensas_populacao[i]
552
+ _pontuacoes_populacao[i] += recompensa
553
+
554
+ if not jogador[3]:
555
+ if not _mortos_anteriores[i] and agente.experiencias_episodio:
556
+ ultimo = agente.experiencias_episodio[-1]
557
+ agente.guardar(ultimo[3], ultimo[1], recompensa, ultimo[3], True)
558
+ agente.aprender()
559
+ agente.encerrar_episodio()
560
+ _mortos_anteriores[i] = True
561
+ return i
562
+
563
+ _mortos_anteriores[i] = False
564
+ if not hasattr(agente, "estado_anterior"):
565
+ agente.estado_anterior = None
566
+ agente.acao_anterior = None
567
+ agente.contador = 0
568
+
569
+ if agente.estado_anterior is not None:
570
+ agente.guardar(agente.estado_anterior, agente.acao_anterior, recompensa, estado, False)
571
+ agente.contador += 1
572
+ if agente.contador % _FREQUENCIA_APRENDIZADO == 0:
573
+ agente.aprender()
574
+
575
+ acao = agente.escolher_acao(estado)
576
+ if acao > 0:
577
+ _funcoes[acao - 1](i)
578
+ agente.estado_anterior = estado
579
+ agente.acao_anterior = acao
580
+ return i
581
+
582
+
583
+ def update():
584
+ """Atualiza a população; decisões e treinamento são espaçados para aliviar a CPU."""
585
+ global _player_pos, _player_size, _player_velocity_y, _melhor_indice
586
+ global _ticks_populacao, _recompensas_acumuladas, _recompensas_populacao
587
+ if not _modo_populacao:
588
+ return _update_individual_original()
589
+ if len(_jogadores) != _QUANTIDADE_JOGADORES or len(_agentes_populacao) != _QUANTIDADE_JOGADORES:
590
+ raise RuntimeError("Chame nn.players() com 100 jogadores antes de nn.update().")
591
+
592
+ _ticks_populacao += 1
593
+ for i, valor in enumerate(_recompensas_populacao):
594
+ _recompensas_acumuladas[i] += valor
595
+
596
+ # A física continua a cada passo, mas a IA só recalcula estados e ações
597
+ # a cada 4 passos (30 decisões/s em vez de 120). Mortes são processadas já.
598
+ houve_morte = any(not item[3] and not _mortos_anteriores[i]
599
+ for i, item in enumerate(_jogadores))
600
+ if _ticks_populacao % _FREQUENCIA_DECISAO != 0 and not houve_morte:
601
+ return True
602
+
603
+ _recompensas_populacao = _recompensas_acumuladas
604
+ _recompensas_acumuladas = [0.0] * _QUANTIDADE_JOGADORES
605
+
606
+ # Monta os estados das 100 IAs em lote. As hitboxes são compartilhadas;
607
+ # NumPy calcula as distâncias relativas sem repetir loops Python gigantes.
608
+ quantidades = (len(_obstaculos), len(_obsnot), len(_paredes))
609
+ if _quantidades_observacao is not None and quantidades != _quantidades_observacao:
610
+ raise RuntimeError("A quantidade de hitboxes mudou depois do início da IA.")
611
+
612
+ jogadores_np = np.asarray([
613
+ [item[0][0], item[0][1], item[2], item[1][0], item[1][1]]
614
+ for item in _jogadores
615
+ ], dtype=np.float32)
616
+ estados_np = np.empty((100, _entradas_populacao), dtype=np.float32)
617
+ estados_np[:, 0] = np.clip(jogadores_np[:, 1] / 600.0, -1.0, 1.0)
618
+ estados_np[:, 1] = np.clip(jogadores_np[:, 2] / 900.0, -1.0, 1.0)
619
+ estados_np[:, 2] = np.clip(jogadores_np[:, 3] / 300.0, 0.0, 1.0)
620
+ estados_np[:, 3] = np.clip(jogadores_np[:, 4] / 300.0, 0.0, 1.0)
621
+
622
+ deslocamento = 4
623
+ for grupo in (_obstaculos, _obsnot, _paredes):
624
+ if grupo:
625
+ hitboxes = np.asarray(grupo, dtype=np.float32)
626
+ quantidade = len(hitboxes)
627
+ bloco = np.empty((100, quantidade, 4), dtype=np.float32)
628
+ bloco[:, :, 0] = np.clip((hitboxes[None, :, 0] - jogadores_np[:, None, 0]) / 1050.0, -10.0, 10.0)
629
+ bloco[:, :, 1] = np.clip((hitboxes[None, :, 1] - jogadores_np[:, None, 1]) / 600.0, -10.0, 10.0)
630
+ bloco[:, :, 2] = np.clip(hitboxes[None, :, 2] / 300.0, 0.0, 10.0)
631
+ bloco[:, :, 3] = np.clip(hitboxes[None, :, 3] / 300.0, 0.0, 10.0)
632
+ estados_np[:, deslocamento:deslocamento + quantidade * 4] = bloco.reshape(100, quantidade * 4)
633
+ deslocamento += quantidade * 4
634
+
635
+ for i in range(100):
636
+ _processar_jogador((i, estados_np[i]))
637
+
638
+ vivos = [i for i, item in enumerate(_jogadores) if item[3]]
639
+ if vivos:
640
+ _melhor_indice = max(vivos, key=lambda i: _pontuacoes_populacao[i])
641
+ return True
642
+
643
+
644
+ def _update_individual_original():
645
+ global _estado_anterior, _acao_anterior, _recompensa_atual, _contador_atualizacoes
646
+ if _player_pos is None or _player_size is None:
647
+ raise RuntimeError("Chame nn.player(posicao, tamanho) antes de nn.update().")
648
+ estado = _obter_estado()
649
+ _garantir_agente(len(estado))
650
+ _contador_atualizacoes += 1
651
+ if _estado_anterior is not None and _acao_anterior is not None:
652
+ _agente.guardar(_estado_anterior, _acao_anterior, _recompensa_atual, estado, False)
653
+ if _contador_atualizacoes % _FREQUENCIA_APRENDIZADO == 0:
654
+ _agente.aprender()
655
+ acao = _agente.escolher_acao(estado)
656
+ if acao > 0:
657
+ _funcoes[acao - 1]()
658
+ _estado_anterior, _acao_anterior = estado, acao
659
+ _recompensa_atual = 1.0
660
+ return True
661
+
662
+
663
+ def reset():
664
+ """No modo populacao, inicia nova geracao a partir da melhor IA."""
665
+ global _estado_anterior, _acao_anterior, _recompensa_atual
666
+ global _total_tentativas, _melhor_recompensa, _geracao
667
+ global _agentes_populacao, _pontuacoes_populacao, _mortos_anteriores
668
+ global _melhor_pontuacao_geral, _ticks_populacao, _recompensas_acumuladas
669
+ if not _modo_populacao:
670
+ return _reset_individual_original()
671
+ if any(item[3] for item in _jogadores):
672
+ return
673
+ melhor_i = max(range(100), key=lambda i: _pontuacoes_populacao[i])
674
+ melhor = _agentes_populacao[melhor_i]
675
+ pontuacao = _pontuacoes_populacao[melhor_i]
676
+ if pontuacao > _melhor_pontuacao_geral:
677
+ _melhor_pontuacao_geral = pontuacao
678
+ try:
679
+ with open(_caminho_melhor_populacao, "wb") as arquivo:
680
+ pickle.dump({"pesos": [p.copy() for p in melhor.rede.parametros()]}, arquivo, protocol=pickle.HIGHEST_PROTOCOL)
681
+ except Exception as erro:
682
+ print("neuralnetwork: erro ao salvar melhor populacao:", erro)
683
+ _agentes_populacao = [_novo_agente_populacao(i, melhor) for i in range(100)]
684
+ _pontuacoes_populacao = [0.0] * 100
685
+ _mortos_anteriores = [False] * 100
686
+ _recompensas_populacao = [0.0] * 100
687
+ _recompensas_acumuladas = [0.0] * 100
688
+ _ticks_populacao = 0
689
+ _geracao += 1
690
+ _total_tentativas += 100
691
+
692
+
693
+ def _reset_individual_original():
694
+ global _estado_anterior, _acao_anterior, _recompensa_atual
695
+ global _total_tentativas, _melhor_recompensa
696
+ if _agente is not None and _estado_anterior is not None and _acao_anterior is not None:
697
+ proximo = _obter_estado() if _player_pos is not None else _estado_anterior
698
+ _agente.guardar(_estado_anterior, _acao_anterior, _recompensa_atual, proximo, True)
699
+ _agente.aprender()
700
+ if _agente.experiencias_episodio:
701
+ soma = sum(item[2] for item in _agente.experiencias_episodio)
702
+ if soma > _melhor_recompensa:
703
+ _melhor_recompensa = soma
704
+ _agente.encerrar_episodio()
705
+ else:
706
+ _agente.experiencias_episodio = deque(maxlen=_MAX_MEMORIA)
707
+ _total_tentativas += 1
708
+ if _total_tentativas % 10 == 0:
709
+ _agente.salvar()
710
+ _estado_anterior = None
711
+ _acao_anterior = None
712
+ _recompensa_atual = 1.0
713
+
714
+
715
+ def population_stats():
716
+ return {"geracao": _geracao, "vivas": sum(1 for p in _jogadores if p[3]),
717
+ "melhor_indice": _melhor_indice, "melhor_pontuacao": max(_pontuacoes_populacao) if _pontuacoes_populacao else 0}