O Carinha passa a melhorar as interações com o que vive em casa, sem IA e sem internet (aprendizado.py): - gosto: gesto ou atividade feito com alguém por perto é uma tentativa; carinho, brincadeira ou conversa até 60 s depois contam como "gostaram" (cutucada vale meio). A chance de cada ato vai de 0,5x a 2x, relativa à média, e só depois de 3 tentativas; - rotina: fração de minutos com gente em cada hora (média móvel entre dias). Antes de uma hora movimentada, acorda do cochilo 15 min antes; sono pesado e madrugada continuam respeitados; - sono: PIR acordou e ninguém ficou nem interagiu em 3 min = à toa. Se repete na mesma hora, ali pede 1 ou 2 disparos a mais para acordar; - à meia-noite fecha o dia: lições em primeira pessoa no diário e o passado pesa 15% menos a cada dia (o gosto muda); - rotas /aprendizado/status|config|esquecer e SSE 'aprendizado'; - painel: cartão "O que ele aprendeu" no Ao vivo (rotina em 24 barras, faz mais / faz menos, lições, esquecer com confirmação) e tópico no guia. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
237 lines
10 KiB
Python
237 lines
10 KiB
Python
"""Aprendizado com o dia a dia: gosto pelos atos, rotina da casa, sono e o fechamento do dia."""
|
|
import http.client
|
|
import importlib.util
|
|
import json
|
|
import random
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
from aprendizado import Aprendizado # noqa: E402
|
|
from vida import Vida # noqa: E402
|
|
|
|
spec = importlib.util.spec_from_file_location('kiosk_aprendizado', ROOT / 'kiosk-server.py')
|
|
kiosk = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(kiosk)
|
|
|
|
|
|
def em(dia, hora, minuto=0, segundo=0):
|
|
return time.mktime((2026, 10, dia, hora, minuto, segundo, 0, 0, -1))
|
|
|
|
|
|
class AprendizadoTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.caminho = Path(self.tmp.name) / 'aprendizado.json'
|
|
self.a = Aprendizado(self.caminho)
|
|
self.a.hoje = self.a._dia_vazio('2026-10-06')
|
|
self.diario = []
|
|
self.a.anotar = lambda tipo, texto, agora=None: self.diario.append((tipo, texto))
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def fazer(self, nome, vezes, resposta=None, inicio=None):
|
|
t = inicio or em(6, 15)
|
|
for i in range(vezes):
|
|
self.a.ato(nome, t + i * 120, perto=True)
|
|
if resposta:
|
|
self.a.resposta(resposta, t + i * 120 + 20)
|
|
|
|
def test_gosto_sobe_com_carinho_e_desce_quando_ignorado(self):
|
|
self.fazer('rir', 5, 'carinho')
|
|
self.fazer('bocejar', 5, inicio=em(6, 16))
|
|
self.assertGreater(self.a.peso('rir'), 1.0)
|
|
self.assertLess(self.a.peso('bocejar'), 1.0)
|
|
self.assertLessEqual(self.a.peso('rir'), 2.0)
|
|
self.assertGreaterEqual(self.a.peso('bocejar'), 0.5)
|
|
|
|
def test_sem_opiniao_com_poucas_tentativas_ou_sozinho(self):
|
|
self.fazer('rir', 2, 'carinho')
|
|
self.assertEqual(self.a.peso('rir'), 1.0)
|
|
for i in range(10):
|
|
self.a.ato('pensar', em(6, 15) + i, perto=False) # sem plateia ninguém pode gostar
|
|
self.assertEqual(self.a.peso('pensar'), 1.0)
|
|
self.assertNotIn('pensar', self.a.itens)
|
|
|
|
def test_resposta_fora_da_janela_nao_conta(self):
|
|
for i in range(4):
|
|
self.a.ato('acenar', em(6, 15) + i * 300, perto=True)
|
|
self.a.resposta('carinho', em(6, 15) + i * 300 + 200) # 200 s depois: já era outra coisa
|
|
self.assertEqual(self.a.itens['acenar']['s'], 0)
|
|
|
|
def test_cada_ato_agrada_uma_vez(self):
|
|
self.a.ato('rir', em(6, 15), perto=True)
|
|
for _ in range(5):
|
|
self.a.resposta('carinho', em(6, 15) + 10)
|
|
self.assertEqual(self.a.itens['rir']['s'], 1.0)
|
|
|
|
def test_fechar_o_dia_gera_licoes_e_esquece_um_pouco(self):
|
|
self.fazer('rir', 4, 'carinho')
|
|
self.fazer('bocejar', 5, inicio=em(6, 17))
|
|
t_antes = self.a.itens['rir']['t']
|
|
self.a.observar(em(7, 0, 1), perto=False) # meia-noite: fecha o dia 6
|
|
textos = [t for _, t in self.diario]
|
|
self.assertTrue(any('gostam de me ver dar risada' in t for t in textos), textos)
|
|
self.assertTrue(any('Tentei bocejar 5 vezes e ninguém ligou' in t for t in textos), textos)
|
|
self.assertAlmostEqual(self.a.itens['rir']['t'], t_antes * 0.85, places=2)
|
|
self.assertEqual(self.a.hoje['dia'], '2026-10-07')
|
|
self.assertTrue(self.caminho.exists())
|
|
|
|
def test_rotina_da_casa_e_acordar_antes_de_chegarem(self):
|
|
for dia in (6, 7, 8):
|
|
for minuto in range(60):
|
|
self.a.observar(em(dia, 17, minuto), perto=False) # 17h: casa vazia
|
|
self.a.observar(em(dia, 18, minuto), perto=True) # 18h: chegou gente
|
|
self.a.observar(em(9, 0, 1), perto=False)
|
|
self.assertGreaterEqual(self.a.rotina[18], 0.5)
|
|
self.assertLess(self.a.rotina[17], 0.3)
|
|
self.assertTrue(any('costumam estar por perto das 18h às 19h' in t for _, t in self.diario), self.diario)
|
|
self.assertTrue(self.a.chega_logo(em(9, 17, 50)))
|
|
self.assertFalse(self.a.chega_logo(em(9, 17, 10))) # ainda falta muito
|
|
self.assertFalse(self.a.chega_logo(em(9, 12, 50)))
|
|
|
|
def test_acordar_a_toa_pede_mais_movimento_naquela_hora(self):
|
|
for dia in (6, 7, 8, 9):
|
|
for vez in range(2):
|
|
t = em(dia, 22, vez * 20)
|
|
self.a.acordou_pelo_sensor(t)
|
|
for s in range(0, 200, 5):
|
|
self.a.observar(t + s, perto=s < 10) # mexeu e sumiu: foi à toa
|
|
self.a.observar(em(dia + 1, 0, 1), perto=False)
|
|
self.assertGreaterEqual(self.a.extra_pir(em(10, 22, 30)), 1)
|
|
self.assertEqual(self.a.extra_pir(em(10, 15)), 0)
|
|
self.assertTrue(any('Acordei à toa 2 vezes por volta das 22h' in t for _, t in self.diario), self.diario)
|
|
|
|
def test_acordar_com_carinho_valeu_a_pena(self):
|
|
t = em(6, 22)
|
|
self.a.acordou_pelo_sensor(t)
|
|
self.a.observar(t + 5, perto=True)
|
|
self.a.resposta('carinho', t + 30)
|
|
for s in range(35, 200, 5):
|
|
self.a.observar(t + s, perto=False)
|
|
self.assertEqual(self.a.hoje['falsos'][22], 0)
|
|
|
|
def test_desligado_nao_aprende_nem_muda_nada(self):
|
|
self.a.configurar(dict(ligado=False))
|
|
self.fazer('rir', 6, 'carinho')
|
|
self.assertEqual(self.a.peso('rir'), 1.0)
|
|
self.assertEqual(self.a.extra_pir(em(6, 22)), 0)
|
|
self.assertFalse(self.a.chega_logo(em(6, 17, 50)))
|
|
|
|
def test_esquecer_e_persistencia(self):
|
|
self.fazer('rir', 5, 'carinho')
|
|
self.fazer('bocejar', 5, inicio=em(6, 17)) # o gosto é relativo: precisa de algo menos querido
|
|
self.a.salvar()
|
|
outro = Aprendizado(self.caminho)
|
|
self.assertGreater(outro.peso('rir'), 1.0)
|
|
outro.anotar = lambda *a, **k: None
|
|
outro.esquecer()
|
|
self.assertEqual(outro.peso('rir'), 1.0)
|
|
self.assertEqual(Aprendizado(self.caminho).itens, {})
|
|
|
|
def test_validar(self):
|
|
with self.assertRaises(ValueError):
|
|
self.a.configurar(dict(ligado='sim'))
|
|
with self.assertRaises(ValueError):
|
|
self.a.configurar(dict(outro=True))
|
|
|
|
|
|
class VidaComAprendizadoTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.a = Aprendizado(Path(self.tmp.name) / 'aprendizado.json')
|
|
self.a.hoje = self.a._dia_vazio('2026-10-06')
|
|
self.v = Vida(Path(self.tmp.name) / 'vida.json', iniciar=False, rng=random.Random(3))
|
|
self.v.aprendizado = self.a
|
|
self.ordens = []
|
|
self.v.enfileirar = self.ordens.append
|
|
self.v.ultimo_tick = em(6, 15)
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def test_gesto_que_agrada_sai_mais(self):
|
|
for i in range(8):
|
|
self.a.ato('rir', em(6, 15) + i * 100, perto=True)
|
|
self.a.resposta('carinho', em(6, 15) + i * 100 + 5)
|
|
self.a.ato('feliz', em(6, 15) + i * 100 + 50, perto=True)
|
|
opcoes = [('rir', 1, ''), ('feliz', 1, '')]
|
|
saiu = [self.v.escolher(opcoes)[0] for _ in range(400)]
|
|
self.assertGreater(saiu.count('rir'), saiu.count('feliz') * 2)
|
|
|
|
def test_carinho_e_atos_com_plateia_chegam_ao_aprendizado(self):
|
|
self.v.alguem_perto = lambda: True
|
|
self.v.celular_online = lambda: True
|
|
self.v.proximo_ato = 0
|
|
self.v.necessidades.update(energia=90, social=90, diversao=90, alegria=90)
|
|
ato = None
|
|
for i in range(30):
|
|
ato = self.v.tick(em(6, 15) + i * 200)
|
|
if ato in self.a.itens:
|
|
break
|
|
self.assertIn(ato, self.a.itens)
|
|
self.v.estimulo('carinho', em(6, 15) + i * 200 + 10)
|
|
self.assertGreater(self.a.itens[ato]['s'], 0)
|
|
|
|
def test_pir_pede_mais_quando_acordou_a_toa_naquela_hora(self):
|
|
self.a.falsos[22] = 1.0
|
|
self.assertFalse(self.v.pir_acorda(em(6, 22)))
|
|
self.assertTrue(self.v.pir_acorda(em(6, 22) + 10))
|
|
self.assertEqual(self.a.acordou_em, em(6, 22) + 10)
|
|
self.assertTrue(self.v.pir_acorda(em(6, 15)))
|
|
|
|
def test_acorda_do_cochilo_antes_de_chegarem(self):
|
|
self.a.rotina[18], self.a.rotina[17] = 0.8, 0.1
|
|
self.v.dormindo_proprio = True
|
|
self.v.necessidades['energia'] = 70
|
|
self.assertEqual(self.v.agir(em(6, 17, 50)), 'acordar')
|
|
self.assertIn('acordar', self.ordens)
|
|
|
|
def test_sono_profundo_nao_e_interrompido_pela_rotina(self):
|
|
self.a.rotina[18], self.a.rotina[17] = 0.8, 0.1
|
|
self.v.dormindo_proprio = True
|
|
self.v.sono_profundo = True
|
|
self.v.necessidades['energia'] = 70
|
|
self.assertIsNone(self.v.agir(em(6, 17, 50)))
|
|
|
|
|
|
class RotasTest(unittest.TestCase):
|
|
def test_status_config_e_esquecer(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
kiosk.ARQUIVO_APARENCIA = str(Path(tmp) / 'a.json')
|
|
s = kiosk.Servidor(('127.0.0.1', 0), kiosk.Handler)
|
|
s.aprendizado = Aprendizado(Path(tmp) / 'ap.json')
|
|
kiosk.ligar_eventos(s)
|
|
threading.Thread(target=s.serve_forever, daemon=True).start()
|
|
try:
|
|
def pedir(metodo, rota, corpo=None):
|
|
c = http.client.HTTPConnection('127.0.0.1', s.server_port, timeout=5)
|
|
dados = json.dumps(corpo).encode() if corpo is not None else None
|
|
c.request(metodo, rota, body=dados, headers={'Content-Type': 'application/json'} if dados else {})
|
|
r = c.getresponse()
|
|
return r.status, json.loads(r.read())
|
|
cod, st = pedir('GET', '/aprendizado/status')
|
|
self.assertEqual(cod, 200)
|
|
self.assertEqual(len(st['rotina']), 24)
|
|
cod, st = pedir('POST', '/aprendizado/config', dict(ligado=False))
|
|
self.assertEqual((cod, st['config']['ligado']), (200, False))
|
|
cod, st = pedir('POST', '/aprendizado/config', dict(ligado='x'))
|
|
self.assertEqual(cod, 400)
|
|
cod, st = pedir('POST', '/aprendizado/esquecer', {})
|
|
self.assertEqual((cod, st['dias']), (200, 0))
|
|
cod, est = pedir('GET', '/estado')
|
|
self.assertIn('aprendizado', est)
|
|
finally:
|
|
s.shutdown()
|
|
kiosk.EVENTOS.fechar()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|