Parado ele vivia escondendo o rosto: a IA local (Qwen 0.6B) reagia a cada som ou "sozinho" a cada 20-60 s e repetia 'esconder' apesar do "varie". - IA local: cada evento só oferece gestos que combinam (barulho: susto ou olhar; movimento: olhar e acenar), os dois últimos ficam fora do enum, no máximo a cada 45 s, não interrompe atividade e, com o modo vida ligado, não reage a "sozinho". - Sem ninguém olhando (sem movimento/PIR há 3 min) nada de gesto "para alguém" (acenar, tímido, esconder...): ele vive a vida dele e começa atividades com mais frequência. - Gestos soltos no celular: quase sempre sutis (olhar de lado, piscar devagar), gesto grande raro e nunca repetido em seguida. - Olhar em repouso com fixações e sacadas em vez do vaivém contínuo. - Tonto: espirais girando no lugar dos olhos (todos os rostos, inclusive Matriz) e boca de "ó". Bocejo: mão na frente da boca com tapinhas e Zzz subindo. As mãos agora aparecem também quando o gesto vem sozinho. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
272 lines
13 KiB
Python
272 lines
13 KiB
Python
"""Modo vida (bichinho virtual): necessidades, sono, atos, figurinhas com limite e rotas HTTP."""
|
|
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 vida import Vida # noqa: E402
|
|
|
|
spec = importlib.util.spec_from_file_location('kiosk_vida', ROOT / 'kiosk-server.py')
|
|
kiosk = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(kiosk)
|
|
|
|
DIA = time.mktime((2026, 10, 6, 15, 0, 0, 0, 0, -1)) # 15h: longe da noite
|
|
|
|
|
|
class TreinoFalso:
|
|
def __init__(self):
|
|
self.pedidos, self.tocadas = [], []
|
|
|
|
def gerar_figurinha(self, assunto):
|
|
self.pedidos.append(assunto)
|
|
|
|
def status(self):
|
|
return dict(reacoes=[dict(id=7, receita=dict(nome='Dança'), feedback=0, ativa=True)])
|
|
|
|
def testar(self, ident):
|
|
self.tocadas.append(ident)
|
|
|
|
|
|
class VidaTest(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.v.enfileirar = self.ordens.append
|
|
self.v.ultimo_tick = DIA
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def test_necessidades_caem_e_sono_recupera(self):
|
|
self.v.necessidades.update(energia=50, social=50, diversao=50)
|
|
self.v.passar_tempo(DIA + 600) # 10 min acordado
|
|
self.assertLess(self.v.necessidades['energia'], 50)
|
|
self.assertLess(self.v.necessidades['diversao'], 50)
|
|
self.v.dormindo_proprio = True
|
|
antes = self.v.necessidades['energia']
|
|
self.v.passar_tempo(DIA + 1200) # 10 min dormindo
|
|
self.assertGreater(self.v.necessidades['energia'], antes)
|
|
|
|
def test_pausa_longa_nao_zera(self):
|
|
self.v.passar_tempo(DIA + 86400) # placa desligada um dia
|
|
self.assertGreater(self.v.necessidades['energia'], 60)
|
|
|
|
def test_cansado_vai_dormir_e_acorda_descansado(self):
|
|
self.v.necessidades['energia'] = 10
|
|
self.assertEqual(self.v.agir(DIA), 'dormir')
|
|
self.assertIn('dormir', self.ordens)
|
|
self.assertTrue(self.v.dormindo())
|
|
self.assertIsNone(self.v.agir(DIA + 60)) # ainda sem energia: continua dormindo
|
|
self.v.necessidades['energia'] = 100
|
|
self.assertEqual(self.v.agir(DIA + 120), 'acordar')
|
|
|
|
def test_madrugada_dorme_mesmo_com_gente_se_mexendo(self):
|
|
noite = time.mktime((2026, 10, 6, 23, 30, 0, 0, 0, -1))
|
|
self.v.alguem_perto = lambda: True # deitado na cama, o PIR vê
|
|
self.assertEqual(self.v.agir(noite), 'dormir')
|
|
self.v.necessidades['energia'] = 100
|
|
self.assertIsNone(self.v.agir(noite + 60)) # descansado, mas é madrugada: não acorda
|
|
self.v.estimulo('carinho', noite + 120) # carinho acorda e segura acordado um tempo
|
|
self.assertFalse(self.v.dormindo())
|
|
self.assertNotEqual(self.v.agir(noite + 180), 'dormir')
|
|
self.assertEqual(self.v.agir(noite + 180 + 11 * 60), 'dormir')
|
|
|
|
def test_ritmo_vira_sensibilidade(self):
|
|
self.v.configurar({'ritmo': 'calmo'})
|
|
self.assertIn('ritmo calmo', self.ordens)
|
|
self.assertFalse(self.v.pir_acorda(DIA)) # calmo: 3 disparos do PIR em 2 min
|
|
self.assertFalse(self.v.pir_acorda(DIA + 30))
|
|
self.assertTrue(self.v.pir_acorda(DIA + 60))
|
|
self.assertFalse(self.v.pir_acorda(DIA + 400)) # os antigos expiraram
|
|
self.v.configurar({'ritmo': 'agitado'})
|
|
self.assertTrue(self.v.pir_acorda(DIA + 500)) # agitado: um basta
|
|
self.ordens.clear()
|
|
self.v.tick(DIA + 505)
|
|
self.assertIn('ritmo agitado', self.ordens) # reenviado junto com o humor
|
|
self.v.configurar({'ligado': False})
|
|
self.assertIn('ritmo normal', self.ordens)
|
|
self.assertTrue(self.v.pir_acorda(DIA + 600)) # desligado: comportamento de sempre
|
|
|
|
def test_atividades_pelo_humor(self):
|
|
self.v.celular_online = lambda: True
|
|
self.v.necessidades.update(energia=80, alegria=70, social=70, diversao=10) # entediado
|
|
self.v.rng.random = lambda: 0.0 # sorteio sempre "sim"
|
|
ato = self.v.agir(DIA) # entediado: vai fazer algo
|
|
self.assertTrue(ato.startswith('atividade '))
|
|
nome, segundos = self.ordens[-1].split()[1:]
|
|
self.assertEqual(ato, 'atividade ' + nome)
|
|
self.assertTrue(120 <= float(segundos) <= 300) # por alguns minutos
|
|
self.assertEqual(self.v.diario[0]['tipo'], 'atividade')
|
|
self.v.celular_atividade = lambda: nome # fazendo: diversão sobe
|
|
self.v.passar_tempo(DIA + 180)
|
|
self.assertGreater(self.v.necessidades['diversao'], 10)
|
|
self.v.celular_atividade = lambda: 'videogame' # ocupado: não interrompe
|
|
self.assertIsNone(self.v.agir(DIA + 60))
|
|
self.v.celular_atividade = lambda: None
|
|
noite = time.mktime((2026, 10, 6, 2, 0, 0, 0, 0, -1))
|
|
self.v.config['noite'] = False
|
|
self.assertFalse(str(self.v.agir(noite)).startswith('atividade')) # de madrugada, nunca
|
|
|
|
def test_exausto_dorme_e_o_sono_e_respeitado(self):
|
|
self.v.necessidades['energia'] = 15
|
|
self.assertEqual(self.v.agir(DIA), 'dormir')
|
|
self.assertTrue(self.v.sono_profundo)
|
|
self.assertIn('sono profundo', self.ordens)
|
|
self.v.necessidades['energia'] = 60
|
|
self.assertIsNone(self.v.agir(DIA + 60)) # ainda não descansou
|
|
self.v.necessidades['energia'] = 96
|
|
self.assertEqual(self.v.agir(DIA + 120), 'acordar')
|
|
self.assertFalse(self.v.sono_profundo)
|
|
self.assertIn('sono leve', self.ordens)
|
|
|
|
def test_acordado_de_proposito_encerra_o_sono_pesado(self):
|
|
self.v.necessidades['energia'] = 15
|
|
self.v.agir(DIA)
|
|
self.v.celular_dormindo = lambda: False # tocaram na tela
|
|
self.v.tick(DIA + 30)
|
|
self.assertFalse(self.v.sono_profundo)
|
|
self.assertEqual(self.v.diario[0]['texto'], 'Acordou antes de descansar')
|
|
|
|
def test_cochilo_quando_cansado(self):
|
|
self.v.celular_online = lambda: True
|
|
self.v.rng.random = lambda: 0.0
|
|
self.v.necessidades.update(energia=35, diversao=80)
|
|
self.assertEqual(self.v.agir(DIA), 'atividade dormir')
|
|
self.v.celular_atividade = lambda: 'dormir' # na caminha: recupera
|
|
antes = self.v.necessidades['energia']
|
|
self.v.passar_tempo(DIA + 300)
|
|
self.assertGreater(self.v.necessidades['energia'], antes)
|
|
|
|
def test_alegria_caindo_deixa_bravo(self):
|
|
self.v.necessidades.update(energia=80, social=80, diversao=80, alegria=100)
|
|
self.assertEqual(self.v.zanga(), 0.0)
|
|
self.v.necessidades['alegria'] = 42
|
|
self.assertEqual(self.v.zanga(), 0.4)
|
|
self.v.tick(DIA + 5)
|
|
self.assertIn('zanga 0.4', self.ordens)
|
|
self.v.necessidades['alegria'] = 20
|
|
self.assertEqual(self.v.humor(), 'bravo')
|
|
self.assertEqual(self.v.zanga(), 0.9)
|
|
self.v.dormindo_proprio = True # dormindo, sem cara feia
|
|
self.assertEqual(self.v.zanga(), 0.0)
|
|
|
|
def test_sozinho_vive_a_vida_dele(self):
|
|
self.v.celular_online = lambda: True
|
|
self.v.alguem_perto = lambda: False
|
|
self.v.visto_em = DIA - 10 * 60 # ninguém há 10 min
|
|
self.v.necessidades.update(energia=80, alegria=40, social=10, diversao=80) # carente
|
|
self.v.rng.random = lambda: 0.99 # sorteios de atividade: não
|
|
for i in range(20):
|
|
gesto = self.v.agir(DIA + i * 100)
|
|
self.assertNotIn(gesto, ('acenar', 'coracao', 'piscadinha', 'timido', 'esconder', 'espiar'))
|
|
self.v.rng.random = lambda: 0.0 # sorteio: sim
|
|
self.v.atividade_em = 0
|
|
self.assertTrue(str(self.v.agir(DIA + 3000)).startswith('atividade ')) # sem plateia: vai fazer algo
|
|
|
|
def test_horario_da_madrugada(self):
|
|
from controle_pc import madrugada
|
|
h = lambda hora: time.mktime((2026, 10, 6, hora, 10, 0, 0, 0, -1))
|
|
cfg = dict(noiteSilenciosa=True, noiteInicio=23, noiteFim=7)
|
|
self.assertEqual([madrugada(cfg, h(x)) for x in (22, 23, 3, 6, 7, 12)], [False, True, True, True, False, False])
|
|
self.assertTrue(madrugada(dict(cfg, noiteInicio=1, noiteFim=5), h(2)))
|
|
self.assertFalse(madrugada(dict(cfg, noiteSilenciosa=False), h(3)))
|
|
|
|
def test_humor_e_gestos_de_carente_com_gente_perto(self):
|
|
self.v.necessidades.update(energia=80, social=10)
|
|
self.assertEqual(self.v.humor(), 'carente')
|
|
self.v.alguem_perto = lambda: True
|
|
gesto = self.v.agir(DIA)
|
|
self.assertIn(gesto, ('acenar', 'espiar', 'coracao', 'surpreso'))
|
|
self.assertEqual(self.v.diario[0]['tipo'], 'acao')
|
|
|
|
def test_cuidados_sobem_necessidades_e_mandam_gesto(self):
|
|
self.v.necessidades['social'] = 20
|
|
st = self.v.estimulo('carinho', DIA)
|
|
self.assertEqual(st['necessidades']['social'], 35)
|
|
self.assertEqual(self.ordens[-1], 'coracao')
|
|
self.v.estimulo('ninar', DIA)
|
|
self.assertTrue(self.v.dormindo())
|
|
with self.assertRaises(ValueError):
|
|
self.v.estimulo('voar', DIA)
|
|
|
|
def test_figurinha_com_limite(self):
|
|
treino = TreinoFalso()
|
|
self.v.treinamento = treino
|
|
self.v.pode_gemini = lambda: True
|
|
self.assertEqual(self.v.motivo_sem_emoji(DIA), '')
|
|
self.assertTrue(self.v.criar_figurinha(DIA, 'feliz'))
|
|
self.assertIn('à tarde', treino.pedidos[0])
|
|
self.assertEqual(self.v.motivo_sem_emoji(DIA + 60), 'descansando a criatividade')
|
|
self.v.config['max_emojis_dia'] = 1
|
|
self.assertEqual(self.v.motivo_sem_emoji(DIA + 4 * 3600), 'limite do dia')
|
|
self.v.pode_gemini = lambda: False
|
|
self.assertEqual(self.v.motivo_sem_emoji(DIA + 4 * 3600), 'sem chave Gemini')
|
|
|
|
def test_tick_manda_humor_e_respeita_bloqueio(self):
|
|
self.v.proximo_ato = DIA + 999
|
|
self.v.tick(DIA + 5)
|
|
self.assertTrue(any(o.startswith('humor ') for o in self.ordens))
|
|
self.ordens.clear()
|
|
self.v.bloqueado = lambda: True
|
|
self.v.proximo_ato = DIA
|
|
self.assertIsNone(self.v.tick(DIA + 70))
|
|
self.assertEqual(self.ordens, [])
|
|
|
|
def test_desligado_nao_age_e_config_persiste(self):
|
|
self.v.configurar({'ligado': False, 'ritmo': 'calmo'})
|
|
self.assertIn('humor normal', self.ordens)
|
|
self.v.proximo_ato = DIA
|
|
self.assertIsNone(self.v.tick(DIA + 10))
|
|
outra = Vida(Path(self.tmp.name) / 'vida.json', iniciar=False)
|
|
self.assertFalse(outra.config['ligado'])
|
|
self.assertEqual(outra.config['ritmo'], 'calmo')
|
|
for ruim in ({'ritmo': 'turbo'}, {'max_emojis_dia': 50}, {'ligado': 1}, {'x': True}):
|
|
with self.assertRaises(ValueError):
|
|
self.v.configurar(ruim)
|
|
|
|
|
|
class VidaHttpTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.server = kiosk.Servidor(('127.0.0.1', 0), kiosk.Handler)
|
|
self.server.token = 'token-vida'
|
|
self.server.vida = Vida(Path(self.tmp.name) / 'vida.json', iniciar=False)
|
|
threading.Thread(target=self.server.serve_forever, daemon=True).start()
|
|
|
|
def tearDown(self):
|
|
self.server.shutdown(); self.server.server_close(); self.tmp.cleanup()
|
|
|
|
def pedir(self, metodo, rota, corpo=None, token='token-vida'):
|
|
c = http.client.HTTPConnection('127.0.0.1', self.server.server_port, timeout=5)
|
|
h = {'Content-Type': 'application/json'}
|
|
if token:
|
|
h['Authorization'] = 'Bearer ' + token
|
|
c.request(metodo, rota, body=json.dumps(corpo) if corpo is not None else None, headers=h)
|
|
r = c.getresponse(); dados = r.read(); c.close()
|
|
return r.status, json.loads(dados)
|
|
|
|
def test_rotas(self):
|
|
self.assertEqual(self.pedir('GET', '/vida/status', token=None)[0], 401)
|
|
self.assertEqual(self.pedir('POST', '/vida/cuidar', {'acao': 'carinho'}, token=None)[0], 401)
|
|
codigo, st = self.pedir('GET', '/vida/status')
|
|
self.assertEqual(codigo, 200)
|
|
self.assertIn(st['humor'], ('feliz', 'tranquilo', 'carente', 'entediado', 'triste', 'cansado', 'dormindo'))
|
|
codigo, st = self.pedir('POST', '/vida/cuidar', {'acao': 'brincar'})
|
|
self.assertEqual((codigo, st['diario'][0]['texto']), (200, 'Brincou'))
|
|
self.assertEqual(self.pedir('POST', '/vida/cuidar', {'acao': 'voar'})[0], 400)
|
|
self.assertFalse(self.pedir('POST', '/vida/config', {'ligado': False})[1]['config']['ligado'])
|
|
self.assertIn('vida', self.pedir('GET', '/estado')[1])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|