"""Modo vida: o Carinha como um bichinho virtual (estilo Tamagotchi). Quatro necessidades (0 a 100, 100 = satisfeito) caem com o tempo e sobem com o que acontece em volta (presença, conversa, carinho). Delas sai o humor, e do humor saem os atos: gestos, cochilos, chamar atenção quando tem gente perto, tocar uma reação da biblioteca e, de vez em quando, desenhar uma figurinha nova com o Gemini. O celular recebe o humor ("humor feliz") e escolhe os gestos soltos dele a partir disso, então o rosto parece ter vida mesmo entre um ato e outro. """ import json import os import random import threading import time from pathlib import Path from comum import gravar_atomico, ler_json NECESSIDADES = ('energia', 'alegria', 'social', 'diversao') # Queda por minuto acordado; dormindo, as outras caem bem mais devagar e a energia volta. QUEDA = dict(energia=0.5, alegria=0.0, social=0.8, diversao=1.0) RECUPERA_SONO = 2.5 # energia por minuto dormindo (25 -> 100 em ~30 min) SONO_LENTO = 0.25 RITMOS = {'calmo': (60, 150), 'normal': (25, 75), 'agitado': (12, 35)} # segundos entre atos # O ritmo também é a sensibilidade: o celular recebe "ritmo x" e ajusta limiares e quantos estímulos # precisa para reagir/acordar. Aqui, quantos disparos do PIR em 2 min acordam o rosto. PIR_ACORDAR = {'calmo': 3, 'normal': 1, 'agitado': 1} PADRAO = dict(ligado=True, criar_emojis=True, max_emojis_dia=3, ritmo='normal', noite=True) NOITE = (23, 7) # horário padrão da madrugada (o servidor usa o dos ajustes do celular) PEDIU_ATENCAO = 10 * 60 # de madrugada, só fica acordado se alguém interagiu (carinho, conversa) há pouco INTERVALO_EMOJI = 90 * 60 # no mínimo 90 min entre duas figurinhas criadas sozinho # O que cada estímulo faz com as necessidades (e o gesto que sai na hora, se houver). ESTIMULOS = { 'pir': (dict(social=4, diversao=2), None), 'movimento': (dict(diversao=3, social=1), None), 'som': (dict(diversao=2), None), 'conversa': (dict(social=12, alegria=6, diversao=4), None), 'carinho': (dict(social=15, alegria=10), 'coracao'), 'cutucar': (dict(social=5, alegria=2, diversao=4), None), 'brincar': (dict(diversao=25, alegria=8, energia=-5), 'alegre'), 'alimentar': (dict(energia=30, alegria=4), 'feliz'), } NOME_ESTIMULO = {'pir': 'O sensor de presença viu alguém', 'movimento': 'A câmera viu movimento', 'som': 'Ouviu um barulho', 'conversa': 'Alguém conversou com ele'} CUIDADOS = {'carinho': 'Recebeu carinho', 'cutucar': 'Levou uma cutucada', 'brincar': 'Brincou', 'alimentar': 'Ganhou energia', 'ninar': 'Foi posto para dormir', 'acordar': 'Foi acordado'} HUMORES = { 'dormindo': ('😴', 'Dormindo'), 'cansado': ('🥱', 'Cansado'), 'carente': ('🥺', 'Carente'), 'entediado': ('😐', 'Entediado'), 'triste': ('😢', 'Triste'), 'bravo': ('😠', 'Mal-humorado'), 'feliz': ('😄', 'Feliz'), 'tranquilo': ('🙂', 'Tranquilo'), } # Gestos de cada humor: (gesto, peso, o que vai no diário) ATOS = { 'feliz': [('alegre', 3, 'Saltitou de alegria'), ('musica', 2, 'Cantarolou'), ('feliz', 3, 'Sorriu à toa'), ('piscadinha', 2, 'Deu uma piscadinha'), ('coracao', 1, 'Ficou apaixonado pelo dia'), ('rir', 2, 'Deu risada sozinho'), ('comemorar', 1, 'Comemorou à toa')], 'tranquilo': [('olhar_esq', 2, 'Olhou em volta'), ('olhar_dir', 2, 'Olhou em volta'), ('piscar2', 2, 'Piscou devagar'), ('feliz', 1, 'Sorriu'), ('pensar', 1, 'Ficou pensativo'), ('timido', 1, 'Ficou tímido')], 'entediado': [('pensar', 3, 'Ficou pensando na vida'), ('olhar_esq', 2, 'Procurou algo para fazer'), ('olhar_dir', 2, 'Procurou algo para fazer'), ('espiar', 2, 'Espiou por trás das mãos'), ('musica', 1, 'Cantou para passar o tempo'), ('bocejar', 2, 'Bocejou de tédio'), ('confuso', 1, 'Ficou confuso com alguma coisa')], 'carente': [('triste', 2, 'Sentiu falta de alguém'), ('olhar_baixo', 2, 'Ficou cabisbaixo'), ('pensar', 1, 'Ficou pensando em alguém')], 'carente_perto': [('acenar', 3, 'Acenou chamando atenção'), ('espiar', 2, 'Espiou quem está aí'), ('coracao', 2, 'Pediu carinho'), ('surpreso', 1, 'Fez cara de "oi!"'), ('timido', 1, 'Ficou tímido'), ('esconder', 1, 'Escondeu o rosto, envergonhado')], 'triste': [('triste', 3, 'Ficou tristinho'), ('olhar_baixo', 2, 'Olhou para baixo'), ('pensar', 1, 'Ficou pensativo')], 'bravo': [('bravo', 3, 'Ficou emburrado'), ('olhar_baixo', 2, 'Bufou de mau humor'), ('confuso', 1, 'Resmungou sozinho')], 'cansado': [('bocejar', 3, 'Bocejou'), ('piscar2', 1, 'Piscou devagar'), ('olhar_baixo', 2, 'Cochilou de olho aberto'), ('tonto', 1, 'Ficou meio tonto de sono')], } # Como no The Sims, cada necessidade baixa puxa um comportamento: # energia < COCHILO: às vezes tira um cochilo (cena da cama); < SONO: dorme de verdade e o sono é # respeitado (som, movimento e PIR não acordam) até DESCANSADO; alegria caindo: o rosto vai ficando # bravo (zanga); diversão < TEDIO: vai fazer uma atividade por alguns minutos. COCHILO, SONO, DESCANSADO, TEDIO = 40, 20, 95, 45 SOZINHO = 3 * 60 # s sem ninguém por perto: ele vive a vida dele (mais atividades, nada de gesto "para alguém") PARA_ALGUEM = {'acenar', 'coracao', 'piscadinha', 'timido', 'esconder', 'espiar', 'surpreso'} ATIVAS = ['bola', 'malhar', 'corda', 'skate', 'dancar', 'pipa'] # gastam energia CALMAS = ['ler', 'cafe', 'meditar', 'telescopio', 'pescar', 'computador', 'regar'] # quase não gastam CRIATIVAS = ['pintar', 'violao', 'cantar', 'cozinhar', 'bolhas', 'videogame'] NOME_ATIVIDADE = { 'ler': 'Foi ler um livro', 'cantar': 'Foi cantar', 'bola': 'Foi jogar bola', 'videogame': 'Foi jogar videogame', 'pintar': 'Foi pintar um quadro', 'malhar': 'Foi malhar', 'dormir': 'Tirou um cochilo', 'cafe': 'Foi tomar um café', 'cozinhar': 'Foi cozinhar', 'regar': 'Foi regar a plantinha', 'violao': 'Foi tocar violão', 'dancar': 'Foi dançar', 'corda': 'Foi pular corda', 'meditar': 'Foi meditar', 'telescopio': 'Foi olhar as estrelas', 'pipa': 'Foi soltar pipa', 'bolhas': 'Foi fazer bolhas de sabão', 'skate': 'Foi andar de skate', 'computador': 'Foi mexer no computador', 'pescar': 'Foi pescar', } # Efeito por minuto enquanto faz a atividade POR_MINUTO_DIVERSAO, POR_MINUTO_ALEGRIA = 6.0, 1.5 ENERGIA_POR_MINUTO = {**{a: -2.0 for a in ATIVAS}, **{a: -0.2 for a in CALMAS}, 'cafe': 3.0} # café dá pique # Assuntos das figurinhas que ele inventa sozinho, por humor e por hora do dia. TEMAS = { 'feliz': ['um sol sorridente', 'um balão colorido', 'um arco-íris', 'um sorvete de casquinha', 'confete de festa'], 'tranquilo': ['uma folha de árvore', 'uma flor', 'um gatinho dormindo', 'uma xícara de chá', 'um vaso com planta'], 'entediado': ['um foguete', 'um planeta com anel', 'uma pipa', 'um dado', 'um controle de videogame', 'um pião'], 'carente': ['um coração com curativo', 'um ursinho de pelúcia', 'uma carta de amor', 'dois corações juntos'], 'triste': ['uma nuvem de chuva', 'um guarda-chuva', 'uma gota d\'água'], 'bravo': ['uma nuvem de tempestade', 'um vulcão', 'um raio', 'uma pimenta'], 'cansado': ['uma lua com touca de dormir', 'uma xícara de café', 'um travesseiro', 'uma bateria fraca'], } PERIODOS = [(5, 12, 'de manhã', ['um sol nascendo', 'um pãozinho', 'uma xícara de café']), (12, 18, 'à tarde', ['um picolé', 'um avião de papel', 'uma bola']), (18, 24, 'à noite', ['uma lua', 'estrelas', 'uma coruja']), (0, 5, 'de madrugada', ['uma lua', 'uma vela', 'uma coruja'])] MAX_DIARIO = 60 # Personalidade: traços de 0 a 1 que mudam o jeito de tudo (0,5 = neutro) e amadurecem com o convívio. TRACOS = ('curiosidade', 'brincadeira', 'timidez') GESTOS_TRACO = {'brincadeira': {'rir', 'comemorar', 'alegre', 'musica', 'piscadinha'}, 'curiosidade': {'olhar_esq', 'olhar_dir', 'pensar', 'espiar', 'confuso'}, 'timidez': {'timido', 'esconder', 'espiar'}} # Reencontro: quanto mais tempo a pessoa ficou fora, maior a festa. AUSENCIA_ACENO, AUSENCIA_OI, AUSENCIA_SAUDADE = 10 * 60, 60 * 60, 4 * 3600 INTERVALO_PEDIDO = 20 * 60 # no mínimo 20 min entre um pedido ("brinca comigo?") e outro DESISTE = 0.12 # chance de largar uma atividade no meio porque ficou chato ESPERA_NOME = 8 # s esperando o reconhecimento antes de cumprimentar sem o nome OI_DE_NOVO = 10 * 60 # reconheceu alguém que não via há esse tempo: "Oi, Ana!" GESTOS_VIDA = {g for atos in ATOS.values() for g, _, _ in atos} def _limitar(v): return max(0.0, min(100.0, v)) class Vida: def __init__(self, caminho, iniciar=True, rng=None): self.caminho = Path(caminho) self.lock = threading.RLock() self.rng = rng or random.Random() self.publicar = lambda tipo, dados: None self.enfileirar = lambda ordem: None # Ligações com o resto do servidor (o main preenche; nos testes ficam neutras). self.treinamento = None self.celular_dormindo = lambda: None # True/False com o celular online, None sem ele self.celular_online = lambda: False self.alguem_perto = lambda: False # PIR/movimento recente self.celular_atividade = lambda: None # atividade que o celular está fazendo agora self.sono_profundo = False # dormindo de cansaço: sensores não acordam self.dormiu_em = 0.0 self.atividade_em = 0.0 # quando começou a última atividade self.ultima_atividade = None self.zanga_enviada = None self.visto_em = time.time() # última vez que alguém esteve por perto self.bloqueado = lambda: False # E-Stop ou modo presença com ninguém em casa self.pode_gemini = lambda: bool(os.environ.get('GEMINI_API_KEY')) self.madrugada = self.noite_padrao # o main troca pelo horário dos ajustes self.interagiu_em = 0.0 self.pir_eventos = [] # Atividades criadas por IA (estúdio e ideias do dia) entram na rotina; as de hoje saem mais. self.novidades = lambda: [] # [{id, nome, emoji, receita, motivo, aprendi, ...}] self.do_dia = lambda: [] # ids das ideias de hoje self.aprendi_pendente = lambda: None # curiosidade de hoje que ainda dá para contar self.contar_aprendi = lambda ident, agora=None: None self.aprendizado = None # o que ele aprende com o dia a dia (aprendizado.py) self.console = lambda origem, texto, motivo='', nivel='info', agora=None: None # console do painel self.dia_anterior = lambda: None # resumo de ontem (aprendizado): para lembrar no bom dia # sorteios dos comportamentos "humanos" ficam à parte: não mexem na sequência do rng principal self.acaso = random.Random() if rng is None else random.Random(7) self.porque = '' # motivo da última decisão (vai para o console) self.ultima = None # {horario, texto, porque}: a última decisão de fato self.perto_antes = False self.ultimo_perto = time.time() self.pediu_em = 0.0 self.desistencia = None # (quando, o que) se largou a atividade no meio self.figurinha_mostrada = 0.0 # reconhecimento de pessoas (reconhecimento.py): o main liga self.reconhece = lambda: False # True: vale esperar a foto para cumprimentar pelo nome self.gostou_de = lambda pessoa, quanto=1.0: None self.reencontro_pendente = None # (desde, ausência) esperando saber quem chegou self.ultimo_reconhecido = None # (id, nome, quando) agora = time.time() self.config = dict(PADRAO) self.necessidades = dict(energia=80.0, alegria=70.0, social=60.0, diversao=60.0) self.nascimento = agora self.diario = [] self.emojis = dict(dia='', total=0, ultimo=0.0) self.dormindo_proprio = False # cada Carinha nasce com um jeito um pouco diferente (nos testes, neutro) self.personalidade = ({k: 0.5 for k in TRACOS} if rng is not None else {k: round(self.acaso.uniform(0.35, 0.65), 2) for k in TRACOS}) self.saudou_dia = '' try: salvo = json.loads(self.caminho.read_text(encoding='utf-8')) self.config.update(self.validar(salvo.get('config', {}))) for k in NECESSIDADES: if isinstance(salvo.get('necessidades', {}).get(k), (int, float)): self.necessidades[k] = _limitar(float(salvo['necessidades'][k])) self.nascimento = float(salvo.get('nascimento', agora)) self.diario = [d for d in salvo.get('diario', []) if isinstance(d, dict)][:MAX_DIARIO] if isinstance(salvo.get('personalidade'), dict): for k in TRACOS: if isinstance(salvo['personalidade'].get(k), (int, float)): self.personalidade[k] = max(0.0, min(1.0, float(salvo['personalidade'][k]))) self.saudou_dia = str(salvo.get('saudou_dia', '')) if isinstance(salvo.get('emojis'), dict): self.emojis.update({k: salvo['emojis'][k] for k in ('dia', 'total', 'ultimo') if k in salvo['emojis']}) except (OSError, ValueError, TypeError, AttributeError): pass self.ultimo_tick = agora self.proximo_ato = agora + 10 self.humor_enviado = (None, 0.0) self.publicado_em = 0.0 self.salvo_em = agora if iniciar: threading.Thread(target=self.loop, daemon=True).start() # ------------------------------------------------------------------ config @staticmethod def validar(dados): if not isinstance(dados, dict): raise ValueError('Esperava um objeto') limpo = {} for chave, valor in dados.items(): if chave not in PADRAO: raise ValueError('Ajuste desconhecido: ' + chave) if chave == 'ritmo': if valor not in RITMOS: raise ValueError('Ritmo: calmo, normal ou agitado') elif chave == 'max_emojis_dia': if isinstance(valor, bool) or not isinstance(valor, int) or not 0 <= valor <= 10: raise ValueError('Figurinhas por dia: de 0 a 10') elif not isinstance(valor, bool): raise ValueError(chave + ' precisa ser verdadeiro ou falso') limpo[chave] = valor return limpo def configurar(self, dados): limpo = self.validar(dados) with self.lock: antes = self.config['ligado'] self.config.update(limpo) if 'ligado' in limpo and limpo['ligado'] != antes: self.anotar('vida', 'Modo autônomo ligado' if limpo['ligado'] else 'Modo autônomo desligado') if not limpo['ligado']: self.enfileirar('humor normal') # o celular volta aos gestos soltos de sempre self.enfileirar('ritmo normal') # e à sensibilidade dos Ajustes self.enfileirar('zanga 0.0') if self.sono_profundo: self.sono_profundo = False self.enfileirar('sono leve') self.proximo_ato = time.time() + 5 if self.config['ligado'] and 'ritmo' in limpo: self.enfileirar('ritmo ' + self.config['ritmo']) self.humor_enviado = (None, 0.0) self.salvar() self.avisar() return self.status() def salvar(self): with self.lock: dados = dict(config=self.config, necessidades=self.necessidades, nascimento=self.nascimento, diario=self.diario, emojis=self.emojis, personalidade=self.personalidade, saudou_dia=self.saudou_dia) self.salvo_em = time.time() try: gravar_atomico(self.caminho, dados) except OSError: pass # ------------------------------------------------------------------ estado def dormindo(self): cel = self.celular_dormindo() return self.dormindo_proprio if cel is None else bool(cel) def humor(self): if self.dormindo(): return 'dormindo' n = self.necessidades if n['energia'] < 25: return 'cansado' if n['social'] < 25: return 'carente' if n['diversao'] < 25: return 'entediado' if n['alegria'] < 30: return 'bravo' if n['alegria'] >= 65 and n['social'] >= 40 and n['diversao'] >= 40: return 'feliz' return 'tranquilo' def zanga(self): """0 a 1: o quanto o rosto está emburrado. Começa abaixo de 60 de alegria e piora devagar.""" if self.dormindo(): return 0.0 return round(max(0.0, min(1.0, (60 - self.necessidades['alegria']) / 45)), 1) def anotar(self, tipo, texto, agora=None): with self.lock: self.diario.insert(0, dict(horario=agora or time.time(), tipo=tipo, texto=texto)) del self.diario[MAX_DIARIO:] def emojis_hoje(self, agora=None): dia = time.strftime('%Y-%m-%d', time.localtime(agora or time.time())) return self.emojis['total'] if self.emojis.get('dia') == dia else 0 def status(self, agora=None): agora = agora or time.time() h = self.humor() with self.lock: return dict(config=dict(self.config), necessidades={k: round(v) for k, v in self.necessidades.items()}, humor=h, emoji=HUMORES[h][0], nome_humor=HUMORES[h][1], dormindo=self.dormindo(), nascimento=self.nascimento, idade_dias=int((agora - self.nascimento) // 86400), emojis_hoje=self.emojis_hoje(agora), ultimo_emoji=self.emojis.get('ultimo') or None, zanga=self.zanga(), sono_profundo=self.sono_profundo, pode_criar=self.motivo_sem_emoji(agora) or '', diario=self.diario[:30], personalidade=dict(self.personalidade), porque=self.porque, ultima=self.ultima, proximo_ato_em=max(0, round(self.proximo_ato - agora))) def avisar(self): self.publicado_em = time.time() self.publicar('vida', self.status()) # ------------------------------------------------------------------ tempo passando def passar_tempo(self, agora): minutos = max(0.0, min(10.0, (agora - self.ultimo_tick) / 60)) # pausa longa (placa desligada) não zera tudo self.ultimo_tick = agora atividade = self.celular_atividade() dormindo = self.dormindo() or atividade == 'dormir' with self.lock: n = self.necessidades for k, q in QUEDA.items(): n[k] = _limitar(n[k] - q * minutos * (SONO_LENTO if dormindo else 1)) if dormindo: n['energia'] = _limitar(n['energia'] + RECUPERA_SONO * minutos) elif atividade: n['diversao'] = _limitar(n['diversao'] + (QUEDA['diversao'] + POR_MINUTO_DIVERSAO) * minutos) n['alegria'] = _limitar(n['alegria'] + POR_MINUTO_ALEGRIA * minutos) n['energia'] = _limitar(n['energia'] + ENERGIA_POR_MINUTO.get(atividade, -0.5) * minutos) # alegria acompanha devagar a média de companhia e diversão alvo = (n['social'] + n['diversao']) / 2 + 10 n['alegria'] = _limitar(n['alegria'] + (alvo - n['alegria']) * min(1.0, 0.05 * minutos)) def estimulo(self, tipo, agora=None): """Algo aconteceu em volta (sensor, conversa, toque, botão do painel).""" if tipo not in ESTIMULOS and tipo not in ('ninar', 'acordar'): raise ValueError('Cuidado desconhecido') agora = agora or time.time() if not self.config['ligado']: return self.status(agora) gesto = None if tipo == 'ninar': self.dormir(agora, 'Foi posto para dormir') elif tipo == 'acordar': self.acordar(agora, 'Foi acordado') else: efeito, gesto = ESTIMULOS[tipo] with self.lock: for k, v in efeito.items(): self.necessidades[k] = _limitar(self.necessidades[k] + v) if tipo in CUIDADOS: self.anotar('cuidado', CUIDADOS[tipo], agora) if tipo in CUIDADOS or tipo == 'conversa': self.interagiu_em = agora # alguém quis ele acordado (sensor não conta) if self.aprendizado: self.aprendizado.resposta(tipo, agora) self.amadurecer(tipo) rec = self.ultimo_reconhecido if rec and agora - rec[2] < 120: self.gostou_de(rec[0], 1.0 if tipo in ('carinho', 'brincar', 'conversa') else 0.5) self.console('sentidos', CUIDADOS.get(tipo) or NOME_ESTIMULO.get(tipo, tipo), ', '.join('%s %+d' % (k, v) for k, v in efeito.items()), 'detalhe' if tipo in ('som', 'movimento') else 'info', agora) if gesto and not self.bloqueado(): if self.dormindo(): self.acordar(agora, None) self.enfileirar(gesto) if tipo in CUIDADOS: self.avisar() return self.status(agora) def dormir(self, agora, motivo, profundo=False): self.dormindo_proprio = True self.dormiu_em = agora self.enfileirar('dormir') if profundo: # cansado de verdade: sono respeitado até descansar self.sono_profundo = True self.enfileirar('sono profundo') if motivo: self.anotar('sono', motivo, agora) def acordar(self, agora, motivo): self.dormindo_proprio = False if self.sono_profundo: self.sono_profundo = False self.enfileirar('sono leve') self.enfileirar('acordar') if motivo: self.anotar('sono', motivo, agora) def escolher_atividade(self): """Pelo pique: cansado prefere coisas calmas (e às vezes um café); cheio de energia, coisas agitadas. As atividades criadas por IA também entram: as ideias de hoje com bastante chance, as antigas às vezes.""" try: novas = [n for n in self.novidades() if 'criado:%d' % n['id'] != self.ultima_atividade] hoje = set(self.do_dia()) except Exception: novas, hoje = [], set() de_hoje = [n for n in novas if n['id'] in hoje] if de_hoje and self.rng.random() < 0.4: return 'criado:%d' % self.rng.choice(de_hoje)['id'] if novas and self.rng.random() < 0.12: return 'criado:%d' % self.rng.choice(novas[:20])['id'] e = self.necessidades['energia'] if 35 <= e < 55 and self.rng.random() < 0.35: return 'cafe' if e < 50: lista = CALMAS * 2 + CRIATIVAS elif e > 70: lista = ATIVAS * 2 + CRIATIVAS + CALMAS else: lista = ATIVAS + CRIATIVAS * 2 + CALMAS opcoes = [a for a in lista if a != self.ultima_atividade] or lista pesos = [self.gosto(a) for a in opcoes] if all(p == 1.0 for p in pesos): return self.rng.choice(opcoes) return self.rng.choices(opcoes, weights=pesos)[0] # as que agradam saem mais def comecar_atividade(self, agora, nome, segundos): if nome.startswith('criado:'): item = next((n for n in self.novidades() if 'criado:%d' % n['id'] == nome), None) if item: receita = dict(item['receita'], duracao=int(segundos)) self.enfileirar('atividade_criada ' + json.dumps(receita, ensure_ascii=False, separators=(',', ':'))) self.atividade_em = agora self.ultima_atividade = nome motivo = ' (' + item['motivo'] + ')' if item.get('motivo') else '' self.anotar('atividade', 'Foi fazer %s %s%s' % (item['emoji'], item['nome'], motivo), agora) return 'atividade ' + nome nome = self.rng.choice(CALMAS) # apagaram a criação no meio do caminho if nome != 'dormir' and segundos > 60 and self.acaso.random() < DESISTE * (1.3 - self.personalidade['curiosidade']): segundos = segundos * self.acaso.uniform(0.3, 0.5) # vai largar no meio: ficou chato self.desistencia = (agora + segundos, NOME_ATIVIDADE.get(nome, 'a atividade')) self.enfileirar('atividade %s %d' % (nome, segundos)) self.atividade_em = agora self.ultima_atividade = nome self.anotar('atividade', NOME_ATIVIDADE.get(nome, 'Foi fazer algo'), agora) return 'atividade ' + nome def dizer(self, texto, gesto=None): """Uma fala curta: o gesto e a frase em legenda, com a voz de robô.""" if gesto: self.enfileirar(gesto) self.enfileirar('dizer ' + texto) def resumo(self): n = self.necessidades return 'energia %d · alegria %d · social %d · diversão %d · %s' % ( n['energia'], n['alegria'], n['social'], n['diversao'], HUMORES[self.humor()][1].lower()) @staticmethod def tempo(seg): return '%d min' % round(seg / 60) if seg < 3600 else '%dh%02d' % (seg // 3600, seg % 3600 // 60) def saudacao(self, agora, nome=None): """Bom dia com uma lembrança de ontem (do que ele viveu, sem IA).""" hora = time.localtime(agora).tm_hour prefixo = 'Bom dia' if 5 <= hora < 12 else 'Boa tarde' if hora < 18 else 'Boa noite' if nome: prefixo += ', ' + nome fatos = [] ontem = None try: ontem = self.dia_anterior() except Exception: pass if isinstance(ontem, dict): pres = ontem.get('presenca') or [] tarde = [hh for hh in range(0, 5) if hh < len(pres) and pres[hh] is not None and pres[hh] >= 0.3] if tarde: fatos.append('Ontem a gente ficou acordado até as %dh, hein!' % (max(tarde) + 1)) elif len(pres) == 24 and pres[23] is not None and pres[23] >= 0.3: fatos.append('Ontem a gente ficou até tarde, hein!') if (ontem.get('respostas') or 0) >= 5: fatos.append('Ontem foi um dia cheio de carinho. Obrigado!') elif (ontem.get('atos') or 0) >= 5 and not ontem.get('respostas'): fatos.append('Ontem ninguém brincou comigo... hoje vai ser diferente?') if not fatos: fatos.append('Acordei cheio de energia!' if self.necessidades['energia'] >= 80 else 'Que bom te ver!') return '%s! %s' % (prefixo, self.acaso.choice(fatos)) def reencontro(self, agora, ausencia, nome=None, afinidade=0.0): """Alguém apareceu: a reação depende de quanto tempo ficou fora (e de quem ele é).""" if self.dormindo() or self.madrugada(agora) or self.bloqueado(): return None n, tim = self.necessidades, self.personalidade['timidez'] dia = time.strftime('%Y-%m-%d', time.localtime(agora)) motivo = ('%s não aparecia havia ' % nome if nome else 'ninguém por perto havia ') + self.tempo(ausencia) querido = afinidade >= 20 # muito carinho dessa pessoa: festa com coração if self.saudou_dia != dia and time.localtime(agora).tm_hour >= 5 and ausencia >= AUSENCIA_ACENO: self.saudou_dia = dia frase = self.saudacao(agora, nome) self.dizer(frase, 'timido' if tim > 0.65 else 'acenar') ganho, texto, motivo = 10, 'Deu bom dia: "%s"' % frase, 'primeiro encontro do dia; ' + motivo elif ausencia >= AUSENCIA_SAUDADE: self.dizer('%s! Senti sua falta!' % nome if nome else 'Senti sua falta!', 'comemorar') self.enfileirar('coracao') ganho, texto = 20, 'Fez festa: estava com saudade' + (' de ' + nome if nome else '') n['alegria'] = _limitar(n['alegria'] + 10) elif ausencia >= AUSENCIA_OI: self.dizer('Oi, %s! Você voltou!' % nome if nome else 'Oi! Você voltou!', 'timido' if tim > 0.65 else 'comemorar') if querido: self.enfileirar('coracao') ganho, texto = 10, 'Comemorou que %s voltou' % (nome or 'alguém') elif ausencia >= AUSENCIA_ACENO: if nome: self.dizer('Oi, %s!' % nome, 'acenar') else: self.enfileirar('acenar') ganho, texto = 4, 'Acenou para %s' % (nome or 'quem voltou') else: return None with self.lock: n['social'] = _limitar(n['social'] + ganho) self.anotar('social', texto, agora) self.console('vida', texto, motivo, agora=agora) self.avisar() return texto def reconhecido(self, pessoa, confianca, agora=None): """O reconhecimento disse quem está aí (vem da thread do reconhecimento).""" agora = agora or time.time() nome = pessoa['nome'] self.ultimo_reconhecido = (pessoa['id'], nome, agora) antes = pessoa.get('visto_antes') pendente = self.reencontro_pendente if pendente: # chegou alguém e agora sabe quem é self.reencontro_pendente = None ausencia = agora - antes if antes else pendente[1] return self.reencontro(agora, ausencia, nome, pessoa.get('afinidade', 0)) if antes and agora - antes > OI_DE_NOVO: # chegou outra pessoa com alguém já por perto return self.reencontro(agora, agora - antes, nome, pessoa.get('afinidade', 0)) return None def pedir_algo(self, agora): """Iniciativa com motivo: pede o que está faltando (ou mostra o que fez).""" n = self.necessidades if n['social'] < 35: frase, gesto, porque = 'Tô carente... me faz um carinho?', 'coracao', 'social %d' % n['social'] elif n['diversao'] < 40: frase, gesto, porque = 'Brinca comigo?', 'acenar', 'diversão %d' % n['diversao'] elif (float(self.emojis.get('ultimo') or 0) > self.figurinha_mostrada and agora - float(self.emojis['ultimo']) < 3 * 3600 and self.tocar_reacao(agora, anotar=False)): self.figurinha_mostrada = float(self.emojis['ultimo']) frase, gesto, porque = 'Olha o que eu desenhei!', None, 'criou uma figurinha e ainda não tinha mostrado' elif n['energia'] < 35: frase, gesto, porque = 'Tô com soninho...', 'bocejar', 'energia %d' % n['energia'] else: return None self.pediu_em = agora self.dizer(frase, gesto) self.anotar('pedido', 'Pediu: "%s"' % frase, agora) self.porque = 'alguém por perto e ' + porque return 'pedido' def pir_acorda(self, agora=None): """Disparo do PIR com o rosto dormindo: acorda? No ritmo calmo precisa de vários em 2 min.""" agora = agora or time.time() self.pir_eventos = [t for t in self.pir_eventos if agora - t < 120] + [agora] precisa = PIR_ACORDAR[self.config['ritmo']] if self.config['ligado'] else 1 extra = self.aprendizado.extra_pir(agora) if self.aprendizado and self.config['ligado'] else 0 if extra and precisa <= len(self.pir_eventos) < precisa + extra: self.aprendizado.segurou_sono(agora, precisa, extra) precisa += extra # nesta hora já acordou à toa outras vezes if len(self.pir_eventos) >= precisa: self.pir_eventos = [] if self.aprendizado and self.config['ligado']: self.aprendizado.acordou_pelo_sensor(agora) return True return False @staticmethod def noite_padrao(agora): hora = time.localtime(agora).tm_hour ini, fim = NOITE return hora >= ini or hora < fim # ------------------------------------------------------------------ decidir o que fazer def gosto(self, nome): """Quanto o aprendizado multiplica a chance de um ato (1 = sem opinião).""" return self.aprendizado.peso(nome) if self.aprendizado else 1.0 def jeito(self, gesto): """Quanto a personalidade puxa um gesto (1 = neutro): brincalhão ri mais, tímido se esconde mais.""" f = 1.0 for traco, gestos in GESTOS_TRACO.items(): if gesto in gestos: f *= 1 + (self.personalidade[traco] - 0.5) * 1.2 return f def amadurecer(self, tipo): """O convívio muda o jeito devagar: carinho deixa menos tímido, brincar deixa mais brincalhão.""" p = self.personalidade with self.lock: if tipo in ('carinho', 'conversa', 'brincar'): p['timidez'] = round(max(0.1, p['timidez'] - 0.003), 3) if tipo == 'brincar': p['brincadeira'] = round(min(0.95, p['brincadeira'] + 0.002), 3) if tipo == 'conversa': p['curiosidade'] = round(min(0.95, p['curiosidade'] + 0.001), 3) def escolher(self, opcoes): opcoes = [(g, p * self.gosto(g) * self.jeito(g), t) for g, p, t in opcoes] total = sum(p for _, p, _ in opcoes) x = self.rng.uniform(0, total) for gesto, peso, texto in opcoes: x -= peso if x <= 0: return gesto, texto return opcoes[-1][0], opcoes[-1][2] def motivo_sem_emoji(self, agora): """'' quando pode criar uma figurinha agora; senão, o porquê (aparece no painel).""" if not self.config['criar_emojis']: return 'desligado' if self.treinamento is None: return 'biblioteca indisponível' if not self.pode_gemini(): return 'sem chave Gemini' if self.emojis_hoje(agora) >= self.config['max_emojis_dia']: return 'limite do dia' if agora - float(self.emojis.get('ultimo') or 0) < INTERVALO_EMOJI: return 'descansando a criatividade' return '' def criar_figurinha(self, agora, humor): h = time.localtime(agora).tm_hour periodo, extras = next((p, e) for ini, fim, p, e in PERIODOS if ini <= h < fim) tema = self.rng.choice(TEMAS.get(humor, TEMAS['tranquilo']) + extras) assunto = (f'Ideia que o próprio Carinha teve sozinho, {periodo}, se sentindo {HUMORES[humor][1].lower()}: ' f'{tema}. Figurinha fofa e simples.') try: self.treinamento.gerar_figurinha(assunto) except ValueError as e: self.anotar('emoji', 'Quis desenhar ' + tema + ', mas não deu: ' + str(e)[:80], agora) return False dia = time.strftime('%Y-%m-%d', time.localtime(agora)) with self.lock: total = self.emojis['total'] + 1 if self.emojis.get('dia') == dia else 1 self.emojis.update(dia=dia, total=total, ultimo=agora) self.necessidades['diversao'] = _limitar(self.necessidades['diversao'] + 30) self.necessidades['alegria'] = _limitar(self.necessidades['alegria'] + 10) self.anotar('emoji', 'Teve uma ideia e desenhou ' + tema, agora) return True def tocar_reacao(self, agora, anotar=True): try: reacoes = [r for r in self.treinamento.status()['reacoes'] if r.get('ativa', True) and r['feedback'] >= 0] except Exception: return False if not reacoes: return False r = self.rng.choice(reacoes[:40]) if anotar else reacoes[0] # para mostrar: a mais nova try: self.treinamento.testar(r['id']) except ValueError: return False if anotar: self.anotar('acao', 'Lembrou da reação "' + r['receita'].get('nome', '?') + '"', agora) return True def agir(self, agora): """Um ato: o que um bichinho com essas necessidades faria agora.""" n = self.necessidades perto = self.alguem_perto() self.porque = '' if self.dormindo(): # de madrugada nunca acorda sozinho (nem com gente se mexendo na cama); de dia, só descansado if n['energia'] >= DESCANSADO and not self.madrugada(agora): self.acordar(agora, 'Acordou descansado') self.porque = 'energia %d: descansou' % n['energia'] return 'acordar' # aprendeu a rotina: acorda do cochilo um pouco antes da hora em que costuma ter gente if (self.aprendizado and not self.sono_profundo and not self.madrugada(agora) and n['energia'] >= 50 and self.aprendizado.chega_logo(agora)): self.acordar(agora, 'Acordou: daqui a pouco costuma chegar alguém') self.porque = self.aprendizado.porque_chegada(agora) self.aprendizado.decidir('acordar', 'Acordou do cochilo antes da hora', self.porque, agora) return 'acordar' return None if self.config['noite'] and self.madrugada(agora) and agora - self.interagiu_em > PEDIU_ATENCAO: self.dormir(agora, 'Ficou tarde, foi dormir') self.porque = 'madrugada e ninguém interagiu nos últimos 10 min' return 'dormir' if n['energia'] < SONO: self.dormir(agora, 'Ficou exausto e foi dormir', profundo=True) self.porque = 'energia %d: abaixo de %d, sono pesado até descansar' % (n['energia'], SONO) return 'dormir' humor = self.humor() if self.celular_atividade(): return None # ocupado com uma atividade: deixa terminar dia = self.celular_online() and not self.madrugada(agora) folga = agora - self.atividade_em > 90 # um respiro entre uma atividade e outra # cansado: um cochilo na caminha de vez em quando (recupera energia sem o sono pesado) if dia and folga and n['energia'] < COCHILO and self.rng.random() < 0.35: self.porque = 'energia %d: abaixo de %d, um cochilo' % (n['energia'], COCHILO) return self.comecar_atividade(agora, 'dormir', self.rng.uniform(120, 240)) # entediado: vai fazer alguma coisa por alguns minutos (pode ser interrompido) if dia and folga and n['diversao'] < TEDIO and self.rng.random() < 0.75: self.porque = 'diversão %d: entediado (abaixo de %d)' % (n['diversao'], TEDIO) return self.comecar_atividade(agora, self.escolher_atividade(), self.rng.uniform(120, 300)) # ninguém olhando há um tempo: vai cuidar da vida dele (ler, pintar, música...) if perto: self.visto_em = agora sozinho = agora - self.visto_em > SOZINHO # nesta hora a casa costuma ficar vazia: um cochilo longo guarda pique para quando chegarem if (dia and folga and sozinho and self.aprendizado and n['energia'] < 85 and self.aprendizado.casa_vazia(agora) and self.rng.random() < 0.3): segundos = self.rng.uniform(600, 1200) self.porque = self.aprendizado.porque_vazia(agora) self.aprendizado.decidir('cochilo', 'Tirou um cochilo de %d min para guardar energia' % round(segundos / 60), self.porque, agora) return self.comecar_atividade(agora, 'dormir', segundos) # alguém por perto e ele aprendeu algo hoje: "Olha o que eu aprendi hoje!" if dia and perto and humor != 'bravo': item = self.aprendi_pendente() if item and self.rng.random() < 0.3: self.contar_aprendi(item['id'], agora) with self.lock: n['social'] = _limitar(n['social'] + 5) self.porque = 'alguém por perto e aprendeu "%s" hoje' % item.get('nome', '?') return 'aprendi' # iniciativa: com gente por perto, pede o que está faltando (tímido pede menos) if (dia and perto and not sozinho and agora - self.pediu_em > INTERVALO_PEDIDO and self.acaso.random() < 0.25 * (1.3 - self.personalidade['timidez'])): pedido = self.pedir_algo(agora) if pedido: return pedido if dia and folga and sozinho and self.rng.random() < 0.4: self.porque = 'ninguém por perto há %s: vai cuidar da vida dele' % self.tempo(agora - self.visto_em) return self.comecar_atividade(agora, self.escolher_atividade(), self.rng.uniform(60, 180)) # bem: de vez em quando, uma atividade curtinha por vontade própria if dia and folga and humor in ('feliz', 'tranquilo') and self.rng.random() < 0.1: self.porque = 'de bem (%s): vontade própria' % humor return self.comecar_atividade(agora, self.escolher_atividade(), self.rng.uniform(40, 90)) # habilidade: inventar uma figurinha nova (raro, com limite por dia e intervalo) if not self.motivo_sem_emoji(agora) and self.celular_online() and self.rng.random() < 0.08: if self.criar_figurinha(agora, humor): self.porque = 'teve uma ideia (até %d figurinhas por dia)' % self.config['max_emojis_dia'] return 'figurinha' # às vezes lembra de uma reação da biblioteca if self.treinamento is not None and humor in ('feliz', 'tranquilo', 'entediado') and self.rng.random() < 0.15: if self.tocar_reacao(agora): with self.lock: n['diversao'] = _limitar(n['diversao'] + 8) self.porque = 'humor %s: lembrou de uma reação da biblioteca' % humor return 'reacao' lista = ATOS['carente_perto'] if humor == 'carente' and perto else ATOS.get(humor, ATOS['tranquilo']) if sozinho: # sem plateia: nada de acenar ou ficar tímido lista = [a for a in lista if a[0] not in PARA_ALGUEM] or ATOS['tranquilo'][:3] gesto, texto = self.escolher(lista) self.porque = 'humor %s%s%s' % ( humor, ' com gente por perto' if perto else ', sozinho' if sozinho else '', '; gosto %.1f×, jeito %.1f×' % (self.gosto(gesto), self.jeito(gesto)) if (self.gosto(gesto), round(self.jeito(gesto), 1)) != (1.0, 1.0) else '') if self.aprendizado and perto: self.aprendizado.escolheu(gesto, agora) self.enfileirar(gesto) if humor == 'entediado': with self.lock: n['diversao'] = _limitar(n['diversao'] + 3) # fazer algo distrai um pouquinho self.anotar('acao', texto, agora) return gesto def aprender_ato(self, ato, agora): """Gestos e atividades feitos com plateia entram no aprendizado (para ver se agradam).""" if not self.aprendizado or not ato: return if ato.startswith('atividade '): nome = ato[len('atividade '):] elif ato in GESTOS_VIDA: nome = ato else: return self.aprendizado.ato(nome, agora, self.alguem_perto()) def tick(self, agora=None): agora = agora or time.time() self.passar_tempo(agora) if not self.config['ligado']: return None if self.aprendizado: self.aprendizado.observar(agora, self.alguem_perto()) ato = None humor = self.humor() # acordaram ele de propósito (toque, painel) antes de descansar: o sono pesado acaba aqui if self.sono_profundo and not self.dormindo() and agora - self.dormiu_em > 15: self.sono_profundo = False self.enfileirar('sono leve') self.anotar('sono', 'Acordou antes de descansar', agora) perto = self.alguem_perto() if perto: if not self.perto_antes: if self.reconhece(): self.reencontro_pendente = (agora, agora - self.ultimo_perto) # espera a foto dizer quem é else: self.reencontro(agora, agora - self.ultimo_perto) self.ultimo_perto = agora self.perto_antes = perto pendente = self.reencontro_pendente if pendente and agora - pendente[0] >= ESPERA_NOME: self.reencontro_pendente = None self.reencontro(agora, pendente[1]) if self.desistencia and agora >= self.desistencia[0]: _, oque = self.desistencia self.desistencia = None if not self.dormindo(): self.enfileirar('bocejar') acao = oque[4:] if oque.startswith('Foi ') else 'a atividade' texto = 'Largou no meio: cansou de ' + acao self.anotar('acao', texto, agora) self.console('vida', texto, 'perdeu a graça (curiosidade %.2f)' % self.personalidade['curiosidade'], agora=agora) if not self.bloqueado(): # o celular escolhe os gestos soltos pelo humor; reenvia de tempos em tempos (pode ter reiniciado) zanga = self.zanga() if humor != self.humor_enviado[0] or agora - self.humor_enviado[1] > 60 or zanga != self.zanga_enviada: self.enfileirar('humor ' + humor) self.enfileirar('ritmo ' + self.config['ritmo']) self.enfileirar('zanga %.1f' % zanga) if self.sono_profundo: self.enfileirar('sono profundo') self.humor_enviado = (humor, agora) self.zanga_enviada = zanga if agora >= self.proximo_ato: antes = self.diario[0]['horario'] if self.diario else None ato = self.agir(agora) self.aprender_ato(ato, agora) if ato: novo = self.diario[0] if self.diario and self.diario[0]['horario'] != antes else None self.ultima = dict(horario=agora, texto=novo['texto'] if novo else ato, porque=self.porque) self.console('vida', novo['texto'] if novo else ato, (self.porque + ' · ' if self.porque else '') + self.resumo(), agora=agora) lo, hi = RITMOS[self.config['ritmo']] self.proximo_ato = agora + self.rng.uniform(lo, hi) if ato or agora - self.publicado_em > 30 or humor != self.humor(): self.avisar() if agora - self.salvo_em > 60: self.salvar() return ato def loop(self): while True: time.sleep(5) try: self.tick() except Exception as e: # nunca derruba a thread print('vida: erro no tick:', e, flush=True) # ------------------------------------------------------------------ HTTP def rotas(self, h, metodo): rota = h.path.split('?', 1)[0] if not rota.startswith('/vida/'): return False try: if metodo == 'GET' and rota == '/vida/status': resultado = self.status() elif metodo == 'POST' and rota == '/vida/config': resultado = self.configurar(ler_json(h, 2048)) elif metodo == 'POST' and rota == '/vida/cuidar': resultado = self.estimulo(ler_json(h, 2048).get('acao')) else: raise ValueError('Rota inválida') h.enviar(json.dumps(resultado, ensure_ascii=False)) except (ValueError, TypeError) as e: h.enviar(json.dumps(dict(erro=str(e)), ensure_ascii=False), codigo=400) return True