O aprendizado passa a entender a logística da semana, não só a hora:
- presença por dia da semana × hora (7×24, média móvel por dia) e
histórico dos últimos 42 dias;
- depois da meia-noite, com 7 dias observados, a IA escolhida (Gemini,
IA local ou Qwen do S10, via Criacao.chamar) cruza os dias e devolve
padrões em frases e um plano da semana (casa / fora / dormindo);
- o plano da IA só é adotado se conferir com 70% das horas medidas;
senão fica como sugestão ("Usar este plano");
- "Conte sua rotina": texto livre vira plano na hora e vale sobre o da
IA; o painel mostra o quanto confere com o que os sensores veem;
- o plano guia o comportamento: acorda do cochilo 15 min antes da
chegada naquele dia da semana e, com a casa vazia, tira cochilos
longos para guardar energia;
- lição da rotina só quando muda 3+ horas ou depois de uma semana, e
some quando já existe plano da semana;
- painel: quadro da semana (o que eu vi / plano), a rotina entendida,
sugestão, contar rotina, analisar agora e a IA da análise diária.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
631 lines
32 KiB
Python
631 lines
32 KiB
Python
"""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'),
|
|
}
|
|
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
|
|
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)
|
|
|
|
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
|
|
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('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)
|
|
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])
|
|
|
|
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)
|
|
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
|
|
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 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
|
|
if self.aprendizado and self.config['ligado']:
|
|
precisa += self.aprendizado.extra_pir(agora) # 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 escolher(self, opcoes):
|
|
opcoes = [(g, p * self.gosto(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):
|
|
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])
|
|
try:
|
|
self.treinamento.testar(r['id'])
|
|
except ValueError:
|
|
return False
|
|
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()
|
|
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')
|
|
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')
|
|
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')
|
|
return 'dormir'
|
|
if n['energia'] < SONO:
|
|
self.dormir(agora, 'Ficou exausto e foi dormir', profundo=True)
|
|
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:
|
|
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:
|
|
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):
|
|
return self.comecar_atividade(agora, 'dormir', self.rng.uniform(600, 1200))
|
|
# 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)
|
|
return 'aprendi'
|
|
if dia and folga and sozinho and self.rng.random() < 0.4:
|
|
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:
|
|
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):
|
|
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)
|
|
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.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)
|
|
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:
|
|
ato = self.agir(agora)
|
|
self.aprender_ato(ato, 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
|