Files
carinha-control-panel/tests/test_humano.py
T
julioandClaude Opus 5.5 2933407b9e feat(vida): jeito de gente e console com o porquê de cada decisão
Console (console.py + tela Console no painel):
- cada parte registra o que fez e o motivo: modo vida (com necessidades
  e humor), sentidos, celular (ordens e o que o rosto fez), IA local,
  aprendizado e ideias do dia; últimas 400 linhas, ao vivo pelo SSE;
- tela com o "agora" (humor, necessidades, o que está fazendo, próxima
  decisão, última decisão, jeito), filtros, detalhes, pausa e busca.

Jeito de gente:
- saudade: a festa cresce com o tempo fora (aceno, "Oi! Você voltou!",
  "Senti sua falta!"); bom dia lembrando de ontem;
- pedidos com motivo ("Brinca comigo?", "Me faz um carinho?", mostrar a
  figurinha nova), no máximo 1 a cada 20 min;
- larga atividade no meio quando fica chato; personalidade (curiosidade,
  brincadeira, timidez) que pesa nos gestos e amadurece com o convívio;
- no rosto: acordar grogue, sonhar com algo do dia (balão no motor),
  hesitar antes de gestos, errar na atividade e ficar sem graça, olhar
  nos olhos e desviar, sorrir de volta (FaceLandmarker no rastreador do
  painel) e reagir à luz (sensor de luz no app 0.10.0);
- ordem "dizer" com legenda e voz de robô.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:37:07 -03:00

159 lines
7.1 KiB
Python

"""Jeito de gente do modo vida: reencontro e saudade, bom dia lembrando de ontem, pedidos com motivo,
largar atividade no meio, personalidade e o console com o porquê de cada decisão."""
import random
import sys
import tempfile
import time
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from console import Console # noqa: E402
from vida import Vida # noqa: E402
DIA = time.mktime((2026, 10, 12, 9, 0, 0, 0, 0, -1)) # uma segunda, 9h
class HumanoTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.v = Vida(Path(self.tmp.name) / 'vida.json', iniciar=False, rng=random.Random(1))
self.ordens, self.linhas = [], []
self.v.enfileirar = self.ordens.append
self.v.console = lambda origem, texto, motivo='', nivel='info', agora=None: self.linhas.append((origem, texto, motivo, nivel))
self.v.celular_online = lambda: True
self.v.celular_dormindo = lambda: False
self.v.madrugada = lambda agora: False
self.perto = False
self.v.alguem_perto = lambda: self.perto
def tearDown(self):
self.tmp.cleanup()
def falas(self):
return [o[6:] for o in self.ordens if o.startswith('dizer ')]
def chegar(self, agora, ausencia):
self.v.ultimo_perto = agora - ausencia
self.v.perto_antes = False
self.perto = True
self.v.tick(agora)
def test_reencontro_cresce_com_o_tempo_fora(self):
self.v.saudou_dia = '2026-10-12' # bom dia já foi dado
self.chegar(DIA, 5 * 60) # 5 min fora: nada
self.assertEqual(self.falas(), [])
self.assertNotIn('acenar', self.ordens)
self.chegar(DIA + 100, 20 * 60) # 20 min: acena
self.assertIn('acenar', self.ordens)
self.chegar(DIA + 200, 2 * 3600) # 2 h: comemora
self.assertEqual(self.falas()[-1], 'Oi! Você voltou!')
social = self.v.necessidades['social']
self.chegar(DIA + 300, 6 * 3600) # 6 h: saudade
self.assertEqual(self.falas()[-1], 'Senti sua falta!')
self.assertIn('coracao', self.ordens)
self.assertGreater(self.v.necessidades['social'], social)
self.assertTrue(any('ninguém por perto havia 6h00' in m for _, _, m, _ in self.linhas))
def test_bom_dia_lembra_de_ontem(self):
ontem = dict(presenca=[None] * 24, respostas=0, atos=8)
ontem['presenca'][1] = 0.6 # ficaram acordados até as 2h
self.v.dia_anterior = lambda: ontem
self.v.acaso = random.Random(0)
self.chegar(DIA, 8 * 3600)
fala = self.falas()[0]
self.assertTrue(fala.startswith('Bom dia! '), fala)
self.assertIn(fala[9:], ['Ontem a gente ficou acordado até as 2h, hein!',
'Ontem ninguém brincou comigo... hoje vai ser diferente?'])
self.assertEqual(self.v.saudou_dia, '2026-10-12')
self.chegar(DIA + 3600, 30 * 60) # mesmo dia: não repete o bom dia
self.assertEqual(len([f for f in self.falas() if f.startswith('Bom dia')]), 1)
# de madrugada não faz festa
self.v.madrugada = lambda agora: True
n = len(self.falas())
self.chegar(DIA + 7200, 6 * 3600)
self.assertEqual(len(self.falas()), n)
def test_pede_o_que_falta_com_motivo_e_intervalo(self):
self.v.saudou_dia = '2026-10-12'
self.perto, self.v.perto_antes, self.v.visto_em = True, True, DIA
self.v.atividade_em = DIA # sem folga para atividades
self.v.necessidades.update(social=30, diversao=80, energia=80, alegria=70)
self.v.acaso = random.Random(3)
pedido = None
for i in range(40):
if self.v.agir(DIA + i) == 'pedido':
pedido = DIA + i
break
self.assertIsNotNone(pedido)
self.assertEqual(self.falas()[-1], 'Tô carente... me faz um carinho?')
self.assertIn('social 30', self.v.porque)
self.assertEqual(self.v.diario[0]['tipo'], 'pedido')
n = len(self.falas())
for i in range(40): # menos de 20 min depois: não pede de novo
self.v.agir(pedido + 60 + i)
self.assertEqual(len(self.falas()), n)
def test_larga_atividade_no_meio_e_conta(self):
self.v.acaso = random.Random(0)
largou = None
for i in range(60):
self.v.desistencia = None
self.v.comecar_atividade(DIA + i, 'ler', 200)
if self.v.desistencia:
largou = self.v.desistencia
break
self.assertIsNotNone(largou)
segundos = int(self.ordens[-1].split()[-1])
self.assertLess(segundos, 110) # mandou ao celular a duração encurtada
self.v.tick(largou[0] + 1)
self.assertIn('bocejar', self.ordens)
self.assertIn('Largou no meio: cansou de ler um livro', [d['texto'] for d in self.v.diario[:3]])
def test_personalidade_muda_o_jeito_e_amadurece(self):
self.assertEqual(self.v.jeito('rir'), 1.0) # neutro nos testes
self.v.personalidade['brincadeira'] = 0.9
self.assertGreater(self.v.jeito('rir'), 1.4)
self.v.personalidade['timidez'] = 0.2
self.assertLess(self.v.jeito('timido'), 0.7)
antes = self.v.personalidade['timidez']
for _ in range(10):
self.v.estimulo('carinho', DIA)
self.assertLess(self.v.personalidade['timidez'], antes)
self.v.salvar()
outra = Vida(Path(self.tmp.name) / 'vida.json', iniciar=False, rng=random.Random(1))
self.assertEqual(outra.personalidade, self.v.personalidade)
self.assertIn('personalidade', self.v.status())
def test_cada_decisao_vai_para_o_console_com_o_porque(self):
self.v.necessidades['energia'] = 10
self.v.proximo_ato = 0
self.v.ultimo_tick = DIA
self.v.tick(DIA)
origem, texto, motivo, _ = self.linhas[-1]
self.assertEqual((origem, texto), ('vida', 'Ficou exausto e foi dormir'))
self.assertIn('energia 10: abaixo de 20', motivo)
self.assertIn('energia 10 · alegria', motivo)
self.v.estimulo('carinho', DIA)
self.assertIn(('sentidos', 'Recebeu carinho'), [(o, t) for o, t, _, _ in self.linhas])
class ConsoleTest(unittest.TestCase):
def test_guarda_as_ultimas_e_publica(self):
c = Console()
publicados = []
c.publicar = lambda tipo, dados: publicados.append((tipo, dados))
for i in range(450):
c.registrar('vida', 'ato %d' % i, 'motivo')
c.registrar('inventada', 'vira sistema', nivel='qualquer')
st = c.status()
self.assertEqual(len(st['linhas']), 400)
self.assertEqual(st['linhas'][-1]['origem'], 'sistema')
self.assertEqual(st['linhas'][-1]['nivel'], 'info')
self.assertEqual(publicados[-1][0], 'console')
self.assertEqual(len(c.status(desde=449)['linhas']), 2)
if __name__ == '__main__':
unittest.main()