Spaces:
Running
Running
| import streamlit as st | |
| import random | |
| import unicodedata | |
| from datetime import date, timedelta # ✅ para controle diário de tentativas falhas | |
| def reset_keys(keys): | |
| for k in keys: | |
| st.session_state.pop(k, None) | |
| def strip_accents(s: str) -> str: # ✅ corrigido '->' (sem entidades HTML) | |
| if not isinstance(s, str): | |
| return s | |
| return "".join(c for c in unicodedata.normalize("NFD", s) if unicodedata.category(c) != "Mn") | |
| # ✅ Helper de estado: garantia defensiva (executa sem impactar sua lógica) | |
| def _init_estado_jogos(): | |
| st.session_state.setdefault("pontuacao", 0) | |
| st.session_state.setdefault("rodadas", 0) | |
| st.session_state.setdefault("ultimo_resultado", None) | |
| # ⬇️ NOVO (Dado): memória de resultados já vistos por configuração de lados | |
| st.session_state.setdefault("dado_resultados_vistos", []) | |
| st.session_state.setdefault("dado_lados_atual", None) | |
| # ⬇️ NOVO (Forca): palavras já concluídas por categoria (evita pontuar repetidas) | |
| # Estrutura: { "FPSO": ["INSPECAO VISUAL", ...], "Estoque e Armazenagens": [...], ... } | |
| st.session_state.setdefault("forca_palavras_vistas", {}) | |
| # ⬇️ NOVO (Forca): letras já clicadas (apagadas/desativadas no teclado) | |
| st.session_state.setdefault("forca_letras_usadas", set()) | |
| # ⬇️ NOVO (Forca): controle diário por rodada perdida (tentativa de jogo falha) | |
| st.session_state.setdefault("forca_falhas_hoje", 0) | |
| st.session_state.setdefault("forca_block_expires_on", None) | |
| st.session_state.setdefault("forca_data_ref", str(date.today())) | |
| st.session_state.setdefault("forca_round_contabilizado", False) # marca se a perda já foi contabilizada nesta rodada | |
| # ⬇️ NOVO (Tesouro): perguntas já concluídas por categoria (evita pontuar repetidas) | |
| # Estrutura: { "FPSO": ["Identifique o item correto...", ...], ... } | |
| st.session_state.setdefault("tesouro_perguntas_vistas", {}) | |
| # Mantém inicialização que você tinha (compatível com o helper) | |
| if "pontuacao" not in st.session_state: | |
| st.session_state.pontuacao = 0 | |
| FORCA_BANK = [ | |
| { | |
| "categoria": "FPSO", | |
| "pergunta": "Qual procedimento prioriza a integridade de carga no deck?", | |
| "resposta": "INSPEÇÃO VISUAL", | |
| }, | |
| { | |
| "categoria": "Estoque e Armazenagens", | |
| "pergunta": "Qual política prioriza itens com menor validade remanescente?", | |
| "resposta": "FEFO", | |
| }, | |
| { | |
| "categoria": "Óleo e Gás", | |
| "pergunta": "Qual documento acompanha movimentação de resíduos perigosos?", | |
| "resposta": "MANIFESTO DE RESÍDUOS", | |
| }, | |
| ] | |
| TESOURO_BANK = [ | |
| { | |
| "categoria": "FPSO", | |
| "pergunta": "Identifique o item correto para amarração segura no deck.", | |
| "pistas": [ | |
| {"texto": "Requer inspeção visual antes do uso.", "correto": True}, | |
| {"texto": "É descartável após uma operação.", "correto": False}, | |
| {"texto": "Possui etiqueta de carga segura com WLL.", "correto": True}, | |
| {"texto": "Não pode ser usado em ambiente offshore.", "correto": False}, | |
| ], | |
| }, | |
| { | |
| "categoria": "Estoque e Armazenagens", | |
| "pergunta": "Determine a política correta para expedição de produtos com validade.", | |
| "pistas": [ | |
| {"texto": "Prioriza vencimento mais próximo.", "correto": True}, | |
| {"texto": "Ignora datas de validade.", "correto": False}, | |
| ], | |
| }, | |
| ] | |
| def jogo_dado(): | |
| _init_estado_jogos() # ✅ garante chaves antes do uso | |
| st.subheader("🎲 Jogo do Dado (Curiosidades)") | |
| curiosidades = [ | |
| "FIFO e FEFO impactam diretamente a acuracidade de estoque.", | |
| "Cross-docking reduz tempos e evita armazenagem desnecessária.", | |
| "WMS integra endereçamento, picking e inventários cíclicos.", | |
| ] | |
| lados = st.slider("Escolha o número de lados do dado:", 6, 20, 8) | |
| # ⬇️ NOVO: ao alterar o número de lados, resetamos os resultados vistos para esta configuração | |
| if st.session_state.dado_lados_atual != lados: | |
| st.session_state.dado_lados_atual = lados | |
| st.session_state.dado_resultados_vistos = [] # limpa histórico para a nova configuração de lados | |
| if st.button("Girar dado"): | |
| resultado = random.randint(1, lados) | |
| st.success(f"🎲 Você rolou: {resultado}") | |
| st.info(curiosidades[resultado % len(curiosidades)]) | |
| # ⬇️ NOVO: só pontua se o resultado ainda NÃO tiver saído nesta configuração de lados | |
| if resultado in st.session_state.dado_resultados_vistos: | |
| st.warning("🔁 Resultado repetido — nenhum ponto adicionado ao ranking.") | |
| else: | |
| st.session_state.dado_resultados_vistos.append(resultado) | |
| st.session_state.pontuacao += 5 | |
| st.balloons() | |
| # Informações úteis | |
| if st.session_state.dado_resultados_vistos: | |
| vistos_fmt = ", ".join(str(v) for v in sorted(st.session_state.dado_resultados_vistos)) | |
| st.caption(f"🔎 Valores únicos já obtidos com {lados} lados: {vistos_fmt}") | |
| st.write(f"Pontuação atual: {st.session_state.pontuacao}") | |
| def jogo_forca_treinamento(): | |
| _init_estado_jogos() # ✅ garante chaves antes do uso | |
| st.subheader("🔤 Jogo da Forca (Treinamento)") | |
| # 🔧 Reset diário dos contadores/bloqueio por rodada perdida | |
| if st.session_state.get("forca_data_ref") != str(date.today()): | |
| st.session_state.forca_data_ref = str(date.today()) | |
| st.session_state.forca_falhas_hoje = 0 | |
| st.session_state.forca_block_expires_on = None | |
| # 🔒 Se bloqueado até amanhã, impedir jogar | |
| block_until = st.session_state.get("forca_block_expires_on") | |
| if block_until and date.today() < block_until: | |
| st.error(f"⏳ Você atingiu 3 tentativas falhas hoje. Tente novamente em {block_until.strftime('%d/%m/%Y')}.") | |
| st.write(f"Pontuação atual: {st.session_state.pontuacao}") | |
| return | |
| categorias = sorted(set(q["categoria"] for q in FORCA_BANK)) | |
| cat_sel = st.selectbox("Categoria:", ["Todas"] + categorias, index=0) | |
| banco_filtrado = [q for q in FORCA_BANK if cat_sel == "Todas" or q["categoria"] == cat_sel] | |
| total = len(banco_filtrado) | |
| # Reset ao trocar categoria ou na primeira carga | |
| if "forca_idx" not in st.session_state or st.session_state.get("forca_cat") != cat_sel: | |
| st.session_state.forca_idx = 0 | |
| st.session_state.forca_cat = cat_sel | |
| reset_keys(["forca_palavra", "letras_descobertas", "tentativas", "letras_erradas", "forca_input", "forca_letras_usadas", "forca_round_contabilizado"]) | |
| if not banco_filtrado: | |
| st.warning("Não há perguntas para esta categoria.") | |
| return | |
| nivel = st.session_state.forca_idx % total | |
| st.write(f"Progresso: Pergunta {nivel + 1}/{total}") | |
| atual = banco_filtrado[nivel] | |
| pergunta, resposta = atual["pergunta"], atual["resposta"].upper() | |
| categoria_atual = atual["categoria"] | |
| # Inicializa rodada da forca | |
| if "forca_palavra" not in st.session_state or st.session_state.get("forca_palavra") != resposta: | |
| st.session_state.forca_palavra = resposta | |
| st.session_state.letras_descobertas = ["_" if c.isalpha() else c for c in resposta] | |
| st.session_state.tentativas = 6 | |
| st.session_state.letras_erradas = [] | |
| st.session_state.forca_letras_usadas = set() # ⬅️ inicia teclado limpo | |
| st.session_state.forca_round_contabilizado = False # ⬅️ nova rodada ainda não contabilizada como falha | |
| st.write(f"Pergunta: {pergunta}") | |
| st.write("Palavra:", " ".join(st.session_state.letras_descobertas)) | |
| st.write(f"Tentativas restantes: {st.session_state.tentativas}") | |
| # ============================== | |
| # 🔤 NOVO: TECLADO DE ALFABETO (compacto) | |
| # ============================== | |
| # CSS para deixar botões mais juntos (afeta globalmente — usar com parcimônia) | |
| st.markdown( | |
| """ | |
| <style> | |
| div.stButton > button { | |
| padding: 6px 8px !important; | |
| margin: 2px !important; | |
| font-size: 14px !important; | |
| } | |
| </style> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |
| # A–Z; comparação sem acentos (ex.: 'O' casa 'Ó', 'C' casa 'Ç') | |
| alfabeto = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ") | |
| # Duas linhas com 13 letras cada (mais compacto) | |
| linhas = [ | |
| alfabeto[0:13], | |
| alfabeto[13:26], | |
| ] | |
| for idx_linha, linha in enumerate(linhas): | |
| cols = st.columns(len(linha)) | |
| for idx_letra, letra in enumerate(linha): | |
| usada = letra in st.session_state.forca_letras_usadas | |
| if cols[idx_letra].button(letra, key=f"forca_btn_{nivel}_{letra}", disabled=usada): | |
| # Marca como usada (apagada) | |
| st.session_state.forca_letras_usadas.add(letra) | |
| # Verifica se a letra existe na palavra (comparando sem acentos) | |
| hit = False | |
| for i, ch in enumerate(st.session_state.forca_palavra): | |
| if strip_accents(ch) == strip_accents(letra): | |
| st.session_state.letras_descobertas[i] = ch | |
| hit = True | |
| if not hit: | |
| # Letra errada: registra e decrementa tentativa | |
| if letra not in st.session_state.letras_erradas: | |
| st.session_state.letras_erradas.append(letra) | |
| st.session_state.tentativas -= 1 | |
| # Rerender após o clique para atualizar visualmente a palavra e tentativas | |
| st.rerun() | |
| # Feedback de letras erradas (visível) | |
| if st.session_state.letras_erradas: | |
| st.caption(f"❌ Letras erradas: {', '.join(st.session_state.letras_erradas)}") | |
| venceu = "_" not in st.session_state.letras_descobertas | |
| perdeu = st.session_state.tentativas <= 0 # ✅ corrigido '<=' (sem entidades HTML) | |
| # ⬇️ NOVO: prepara lista de palavras já vistas por categoria | |
| # normaliza removendo acentos para evitar duplicidades por grafia | |
| normalizada = strip_accents(resposta) | |
| if venceu: | |
| # Checa repetição por categoria e evita somar pontos | |
| vistas = st.session_state.forca_palavras_vistas.get(categoria_atual, []) | |
| if normalizada in vistas: | |
| st.warning("🔁 Palavra repetida nesta categoria — nenhum ponto adicionado ao ranking.") | |
| else: | |
| vistas.append(normalizada) | |
| st.session_state.forca_palavras_vistas[categoria_atual] = vistas | |
| st.success("🎉 Você venceu! Palavra completa.") | |
| st.session_state.pontuacao += 10 | |
| st.balloons() | |
| # Mostrar resumo de palavras únicas concluídas nesta categoria | |
| if st.session_state.forca_palavras_vistas.get(categoria_atual): | |
| lista_fmt = ", ".join(st.session_state.forca_palavras_vistas[categoria_atual]) | |
| st.caption(f"🔎 Palavras únicas concluídas ({categoria_atual}): {lista_fmt}") | |
| if nivel + 1 < total: # ✅ corrigido '<' (sem entidades HTML) | |
| if st.button("➡️ Próxima pergunta"): | |
| st.session_state.forca_idx += 1 | |
| reset_keys(["forca_palavra", "letras_descobertas", "tentativas", "letras_erradas", "forca_input", "forca_letras_usadas", "forca_round_contabilizado"]) | |
| st.rerun() | |
| else: | |
| st.success("🏆 Você finalizou todas as perguntas do jogo da Forca!") | |
| elif perdeu: | |
| # ✅ NOVO: contabiliza tentativa falha (rodada perdida) uma única vez | |
| if not st.session_state.forca_round_contabilizado: | |
| st.session_state.forca_falhas_hoje += 1 | |
| st.session_state.forca_round_contabilizado = True | |
| # 🔒 Bloqueia após 3 tentativas falhas no dia | |
| if st.session_state.forca_falhas_hoje >= 3: | |
| st.session_state.forca_block_expires_on = date.today() + timedelta(days=1) | |
| st.error( | |
| f"⏳ Limite diário atingido (3 tentativas falhas). " | |
| f"Tente novamente em {st.session_state.forca_block_expires_on.strftime('%d/%m/%Y')}." | |
| ) | |
| st.error(f"💀 Você perdeu! A palavra era: {st.session_state.forca_palavra}") | |
| st.write(f"Pontuação atual: {st.session_state.pontuacao}") | |
| def jogo_tesouro_niveis(): | |
| _init_estado_jogos() # ✅ garante chaves antes do uso | |
| st.subheader("🗺️ Caça ao Tesouro (Níveis)") | |
| categorias = sorted(set(q["categoria"] for q in TESOURO_BANK)) | |
| cat_sel = st.selectbox("Categoria:", ["Todas"] + categorias, index=0) | |
| banco_filtrado = [q for q in TESOURO_BANK if cat_sel == "Todas" or q["categoria"] == cat_sel] | |
| total = min(100, len(banco_filtrado)) | |
| # Reset ao trocar categoria ou primeira carga | |
| if "tes_idx" not in st.session_state or st.session_state.get("tes_cat") != cat_sel: | |
| st.session_state.tes_idx = 0 | |
| st.session_state.tes_cat = cat_sel | |
| reset_keys(["tes_respostas", "tes_concluido"]) | |
| if total == 0: | |
| st.warning("Não há perguntas para esta categoria.") | |
| return | |
| nivel = st.session_state.tes_idx % total | |
| st.write(f"Progresso: Nível {nivel + 1}/{total}") | |
| atual = banco_filtrado[nivel] | |
| pergunta_atual = atual["pergunta"] | |
| categoria_atual = atual["categoria"] | |
| st.write(f"Pergunta: {pergunta_atual}") | |
| if "tes_respostas" not in st.session_state: | |
| st.session_state.tes_respostas = {i: None for i in range(len(atual["pistas"]))} | |
| st.session_state.tes_concluido = False | |
| for i, pista in enumerate(atual["pistas"]): | |
| cols = st.columns([6, 1, 1]) | |
| cols[0].write(f"🧩 {pista['texto']}") | |
| if cols[1].button("Sim", key=f"tes_sim_{nivel}_{i}"): | |
| st.session_state.tes_respostas[i] = True | |
| if cols[2].button("Não", key=f"tes_nao_{nivel}_{i}"): | |
| st.session_state.tes_respostas[i] = False | |
| r = st.session_state.tes_respostas[i] | |
| if r is not None: | |
| if r == pista["correto"]: | |
| cols[0].success("✅ Correto") | |
| else: | |
| cols[0].error("❌ Incorreto") | |
| tudo_respondido = all(v is not None for v in st.session_state.tes_respostas.values()) | |
| tudo_correto = tudo_respondido and all( | |
| st.session_state.tes_respostas[i] == p["correto"] for i, p in enumerate(atual["pistas"]) | |
| ) | |
| # ⬇️ NOVO: prepara lista de perguntas já concluídas por categoria | |
| pergunta_norm = strip_accents(pergunta_atual).upper() | |
| if tudo_correto: | |
| st.session_state.tes_concluido = True | |
| # Checa repetição por categoria e evita somar pontos | |
| vistas = st.session_state.tesouro_perguntas_vistas.get(categoria_atual, []) | |
| if pergunta_norm in vistas: | |
| st.warning("🔁 Nível/pergunta já concluído anteriormente — nenhum ponto adicionado ao ranking.") | |
| else: | |
| vistas.append(pergunta_norm) | |
| st.session_state.tesouro_perguntas_vistas[categoria_atual] = vistas | |
| st.success("🎉 Parabéns! Nível concluído.") | |
| st.session_state.pontuacao += 10 | |
| st.balloons() | |
| # Mostrar resumo de perguntas únicas concluídas nesta categoria | |
| if st.session_state.tesouro_perguntas_vistas.get(categoria_atual): | |
| lista_fmt = ", ".join(st.session_state.tesouro_perguntas_vistas[categoria_atual]) | |
| st.caption(f"🔎 Perguntas únicas concluídas ({categoria_atual}): {lista_fmt}") | |
| if nivel + 1 < total: # ✅ corrigido '<' (sem entidades HTML) | |
| if st.button("➡️ Avançar para o próximo nível"): | |
| st.session_state.tes_idx += 1 | |
| reset_keys(["tes_respostas", "tes_concluido"]) | |
| st.experimental_rerun() | |
| else: | |
| st.success("🏆 Você finalizou todos os níveis do Caça ao Tesouro!") | |
| st.write(f"Pontuação atual: {st.session_state.pontuacao}") | |
| def main(): | |
| _init_estado_jogos() # ✅ garante estado ao entrar na página Jogos | |
| st.title("🎮 Jogos Interativos para Treinamento") | |
| jogo = st.selectbox( | |
| "Escolha um jogo:", | |
| ["Jogo do Dado (Curiosidades)", "Jogo da Forca (Treinamento)", "Caça ao Tesouro (Níveis)"], | |
| ) | |
| if jogo == "Jogo do Dado (Curiosidades)": | |
| jogo_dado() | |
| elif jogo == "Jogo da Forca (Treinamento)": | |
| jogo_forca_treinamento() | |
| elif jogo == "Caça ao Tesouro (Níveis)": | |
| jogo_tesouro_niveis() | |
| if __name__ == "__main__": | |
| main() | |