CONFIGURAÇÕES 2
This commit is contained in:
parent
9f27edfd4b
commit
36fc77f98e
Binary file not shown.
Binary file not shown.
Binary file not shown.
49
core/migrations/0001_initial.py
Normal file
49
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-18 01:18
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AdminAccess',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('private_key', models.CharField(default=uuid.uuid4, max_length=255, unique=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Lottery',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(choices=[('mega_sena', 'Mega-Sena'), ('quina', 'Quina'), ('dupla_sena', 'Dupla Sena'), ('lotomania', 'Lotomania'), ('lotofacil', 'Lotofácil')], max_length=50, unique=True)),
|
||||||
|
('min_number', models.IntegerField(default=1)),
|
||||||
|
('max_number', models.IntegerField()),
|
||||||
|
('numbers_to_draw', models.IntegerField()),
|
||||||
|
('annulled_numbers', models.TextField(blank=True, default='')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DrawResult',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('draw_number', models.IntegerField()),
|
||||||
|
('draw_date', models.DateField()),
|
||||||
|
('numbers', models.CharField(max_length=255)),
|
||||||
|
('lottery', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='draws', to='core.lottery')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['-draw_date'],
|
||||||
|
'unique_together': {('lottery', 'draw_number')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
@ -1,3 +1,44 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
import uuid
|
||||||
|
|
||||||
# Create your models here.
|
class AdminAccess(models.Model):
|
||||||
|
"""Armazena a chave privada única para acesso ao painel."""
|
||||||
|
private_key = models.CharField(max_length=255, unique=True, default=uuid.uuid4)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Key created on {self.created_at}"
|
||||||
|
|
||||||
|
class Lottery(models.Model):
|
||||||
|
"""Configurações específicas de cada tipo de loteria."""
|
||||||
|
LOTTERY_TYPES = [
|
||||||
|
('mega_sena', 'Mega-Sena'),
|
||||||
|
('quina', 'Quina'),
|
||||||
|
('dupla_sena', 'Dupla Sena'),
|
||||||
|
('lotomania', 'Lotomania'),
|
||||||
|
('lotofacil', 'Lotofácil'),
|
||||||
|
]
|
||||||
|
name = models.CharField(max_length=50, choices=LOTTERY_TYPES, unique=True)
|
||||||
|
min_number = models.IntegerField(default=1)
|
||||||
|
max_number = models.IntegerField()
|
||||||
|
numbers_to_draw = models.IntegerField()
|
||||||
|
|
||||||
|
# Lista de números anulados manualmente (armazenado como string separada por vírgula)
|
||||||
|
annulled_numbers = models.TextField(default="", blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.get_name_display()
|
||||||
|
|
||||||
|
class DrawResult(models.Model):
|
||||||
|
"""Resultados reais dos sorteios da Caixa."""
|
||||||
|
lottery = models.ForeignKey(Lottery, on_delete=models.CASCADE, related_name='draws')
|
||||||
|
draw_number = models.IntegerField()
|
||||||
|
draw_date = models.DateField()
|
||||||
|
numbers = models.CharField(max_length=255) # Ex: "05,12,34,45,56,59"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('lottery', 'draw_number')
|
||||||
|
ordering = ['-draw_date']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.lottery.name} - Concurso {self.draw_number}"
|
||||||
|
|||||||
21
core/templates/core/admin_dashboard.html
Normal file
21
core/templates/core/admin_dashboard.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-5 pt-5">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2>Painel Administrativo</h2>
|
||||||
|
<a href="{% url 'admin_logout' %}" class="btn btn-ghost">Sair</a>
|
||||||
|
</div>
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for lottery in loterias %}
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card-soft p-4 h-100">
|
||||||
|
<h3>{{ lottery.get_name_display }}</h3>
|
||||||
|
<p class="text-muted">Números: 1 a {{ lottery.max_number }}</p>
|
||||||
|
<hr>
|
||||||
|
<a href="{% url 'edit_lottery' lottery.id %}" class="btn btn-brand w-100">Editar Números</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
25
core/templates/core/admin_login.html
Normal file
25
core/templates/core/admin_login.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-5 pt-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card-glass p-5 text-center">
|
||||||
|
<h2 class="mb-4">Acesso Administrativo</h2>
|
||||||
|
<p class="text-muted">Insira sua Private Key para acessar as configurações.</p>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<input type="password" name="private_key" class="form-control form-control-lg text-center" placeholder="Chave Privada" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-brand btn-lg w-100">Acessar Painel</button>
|
||||||
|
</form>
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-danger mt-3">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
70
core/templates/core/edit_lottery.html
Normal file
70
core/templates/core/edit_lottery.html
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-5 pt-5">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'admin_dashboard' %}">Painel</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ lottery.get_name_display }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="card-glass p-4 mb-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2>Editor: {{ lottery.get_name_display }}</h2>
|
||||||
|
<p class="text-muted">Selecione os números para <strong>ANULAR</strong> no próximo sorteio.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="lottery-grid mb-4">
|
||||||
|
{% for n in numbers %}
|
||||||
|
<div class="form-check number-toggle">
|
||||||
|
<input class="form-check-input d-none" type="checkbox" name="numbers" value="{{ n }}" id="num_{{ n }}" {% if n in annulled %}checked{% endif %}>
|
||||||
|
<label class="number-ball {% if n in annulled %}annulled{% endif %}" for="num_{{ n }}">
|
||||||
|
{{ n|stringformat:"02d" }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-brand btn-lg">Salvar Configurações</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.lottery-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
.number-ball {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #0b1f2a;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border: 2px solid rgba(255,255,255,0.1);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.number-ball:hover {
|
||||||
|
background: #0f766e;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
.number-toggle input:checked + .number-ball {
|
||||||
|
background: #dc3545 !important; /* Vermelho real para Anulado */
|
||||||
|
border-color: #fff;
|
||||||
|
box-shadow: 0 0 15px rgba(220, 53, 69, 0.5);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
@ -18,7 +18,11 @@
|
|||||||
<li class="nav-item"><a class="nav-link" href="#simulador">Simulador</a></li>
|
<li class="nav-item"><a class="nav-link" href="#simulador">Simulador</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#jogos">Jogos</a></li>
|
<li class="nav-item"><a class="nav-link" href="#jogos">Jogos</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#como-funciona">Como funciona</a></li>
|
<li class="nav-item"><a class="nav-link" href="#como-funciona">Como funciona</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/admin/">Admin</a></li>
|
{% if is_admin %}
|
||||||
|
<li class="nav-item"><a class="nav-link text-warning" href="{% url 'admin_dashboard' %}">Painel Admin</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'admin_login' %}">Acesso Admin</a></li>
|
||||||
|
{% endif %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="btn btn-brand" href="#simulador">Gerar jogos</a>
|
<a class="btn btn-brand" href="#simulador">Gerar jogos</a>
|
||||||
</li>
|
</li>
|
||||||
@ -126,19 +130,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="number-groups">
|
<div class="number-groups">
|
||||||
<div>
|
<div>
|
||||||
<div class="group-title">Quentes no recorte</div>
|
<div class="group-title text-success">Probabilidades Quentes (Verde)</div>
|
||||||
<div class="badge-grid">
|
<div class="badge-grid">
|
||||||
{% for number in result.hot_numbers %}
|
{% for number in result.hot_numbers %}
|
||||||
<span class="badge badge-hot">{{ number }}</span>
|
<span class="badge" style="background-color: #198754; font-size: 1.1rem;">{{ number|stringformat:"02d" }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="mt-3">
|
||||||
<div class="group-title">Frias no recorte</div>
|
<div class="group-title text-danger">Números Anulados (Vermelho)</div>
|
||||||
<div class="badge-grid">
|
<div class="badge-grid">
|
||||||
{% for number in result.cold_numbers %}
|
{% for number in result.annulled_numbers %}
|
||||||
<span class="badge badge-cold">{{ number }}</span>
|
<span class="badge" style="background-color: #dc3545; font-size: 1.1rem;">{{ number|stringformat:"02d" }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if not result.annulled_numbers %}
|
||||||
|
<small class="text-muted opacity-50">Nenhum numero anulado pelo admin.</small>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
from .views import home
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", home, name="home"),
|
path('', views.home, name='home'),
|
||||||
|
path('admin-loto/', views.admin_login, name='admin_login'),
|
||||||
|
path('admin-loto/logout/', views.admin_logout, name='admin_logout'),
|
||||||
|
path('admin-loto/dashboard/', views.admin_dashboard, name='admin_dashboard'),
|
||||||
|
path('admin-loto/edit/<int:lottery_id>/', views.edit_lottery, name='edit_lottery'),
|
||||||
]
|
]
|
||||||
|
|||||||
229
core/views.py
229
core/views.py
@ -5,121 +5,95 @@ import random
|
|||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
|
||||||
from django import get_version as django_version
|
from django import get_version as django_version
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.contrib import messages
|
||||||
|
|
||||||
from .forms import LotterySimulatorForm
|
from .forms import LotterySimulatorForm
|
||||||
|
from .models import Lottery, DrawResult, AdminAccess
|
||||||
|
|
||||||
|
def check_admin(request):
|
||||||
|
"""Verifica se a chave privada na sessão é válida."""
|
||||||
|
key = request.session.get('admin_key')
|
||||||
|
return AdminAccess.objects.filter(private_key=key).exists()
|
||||||
|
|
||||||
LOTTERY_CONFIGS = {
|
def admin_login(request):
|
||||||
"mega_sena": {
|
"""Tela de login simples para a Chave Privada."""
|
||||||
"label": "Mega-Sena",
|
if request.method == 'POST':
|
||||||
"range_max": 60,
|
key = request.POST.get('private_key')
|
||||||
"picks": 6,
|
if AdminAccess.objects.filter(private_key=key).exists():
|
||||||
"sample_draws": 15,
|
request.session['admin_key'] = key
|
||||||
"seed": 1982,
|
return redirect('admin_dashboard')
|
||||||
"tagline": "6 dezenas entre 60",
|
else:
|
||||||
},
|
messages.error(request, "Chave Privada Inválida!")
|
||||||
"quina": {
|
return render(request, 'core/admin_login.html')
|
||||||
"label": "Quina",
|
|
||||||
"range_max": 80,
|
|
||||||
"picks": 5,
|
|
||||||
"sample_draws": 15,
|
|
||||||
"seed": 1971,
|
|
||||||
"tagline": "5 dezenas entre 80",
|
|
||||||
},
|
|
||||||
"dupla_sena": {
|
|
||||||
"label": "Dupla Sena",
|
|
||||||
"range_max": 50,
|
|
||||||
"picks": 6,
|
|
||||||
"sample_draws": 15,
|
|
||||||
"seed": 1990,
|
|
||||||
"tagline": "6 dezenas entre 50",
|
|
||||||
},
|
|
||||||
"lotomania": {
|
|
||||||
"label": "Lotomania",
|
|
||||||
"range_max": 100,
|
|
||||||
"picks": 50,
|
|
||||||
"sample_draws": 12,
|
|
||||||
"seed": 1999,
|
|
||||||
"tagline": "50 dezenas entre 100",
|
|
||||||
},
|
|
||||||
"lotofacil": {
|
|
||||||
"label": "Lotofacil",
|
|
||||||
"range_max": 25,
|
|
||||||
"picks": 15,
|
|
||||||
"sample_draws": 20,
|
|
||||||
"seed": 1994,
|
|
||||||
"tagline": "15 dezenas entre 25",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
def admin_logout(request):
|
||||||
|
request.session.flush()
|
||||||
|
return redirect('home')
|
||||||
|
|
||||||
def _generate_sample_draws(config):
|
def admin_dashboard(request):
|
||||||
rng = random.Random(config["seed"])
|
"""Painel principal do administrador."""
|
||||||
draws = []
|
if not check_admin(request):
|
||||||
population = list(range(1, config["range_max"] + 1))
|
return redirect('admin_login')
|
||||||
for _ in range(config["sample_draws"]):
|
|
||||||
draws.append(sorted(rng.sample(population, config["picks"])))
|
|
||||||
return draws
|
|
||||||
|
|
||||||
|
loterias = Lottery.objects.all()
|
||||||
|
return render(request, 'core/admin_dashboard.html', {'loterias': loterias})
|
||||||
|
|
||||||
def _weighted_unique_sample(numbers, weights, picks, rng):
|
def edit_lottery(request, lottery_id):
|
||||||
available = list(zip(numbers, weights))
|
"""Editor específico para cada jogo (anular números)."""
|
||||||
selection = []
|
if not check_admin(request):
|
||||||
for _ in range(picks):
|
return redirect('admin_login')
|
||||||
total_weight = sum(weight for _, weight in available)
|
|
||||||
if total_weight <= 0:
|
|
||||||
choice = rng.choice(available)
|
|
||||||
selection.append(choice[0])
|
|
||||||
available.remove(choice)
|
|
||||||
continue
|
|
||||||
pick = rng.uniform(0, total_weight)
|
|
||||||
cumulative = 0
|
|
||||||
for index, (number, weight) in enumerate(available):
|
|
||||||
cumulative += weight
|
|
||||||
if cumulative >= pick:
|
|
||||||
selection.append(number)
|
|
||||||
del available[index]
|
|
||||||
break
|
|
||||||
return selection
|
|
||||||
|
|
||||||
|
lottery = get_object_or_404(Lottery, id=lottery_id)
|
||||||
|
# Ajuste para Lotomania (0-99 se necessário, mas mantendo 1-100 por padrão)
|
||||||
|
numbers = range(1, lottery.max_number + 1)
|
||||||
|
annulled = [int(n) for n in lottery.annulled_numbers.split(',') if n]
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
selected_numbers = request.POST.getlist('numbers')
|
||||||
|
lottery.annulled_numbers = ",".join(selected_numbers)
|
||||||
|
lottery.save()
|
||||||
|
messages.success(request, f"Configurações da {lottery.get_name_display()} salvas!")
|
||||||
|
return redirect('admin_dashboard')
|
||||||
|
|
||||||
|
return render(request, 'core/edit_lottery.html', {
|
||||||
|
'lottery': lottery,
|
||||||
|
'numbers': numbers,
|
||||||
|
'annulled': annulled
|
||||||
|
})
|
||||||
|
|
||||||
def _format_odds(total_combinations):
|
def _format_odds(total_combinations):
|
||||||
if total_combinations >= 1_000_000_000_000:
|
if total_combinations >= 1_000_000_000_000:
|
||||||
return f"1 em {total_combinations:.2e}"
|
return f"1 em {total_combinations:.2e}"
|
||||||
return f"1 em {total_combinations:,}".replace(",", ".")
|
return f"1 em {total_combinations:,}".replace(",", ".")
|
||||||
|
|
||||||
|
|
||||||
def _format_percent(odds):
|
def _format_percent(odds):
|
||||||
percent = 100 / odds
|
percent = 100 / odds
|
||||||
if percent < 0.000001:
|
if percent < 0.000001:
|
||||||
return "<0,000001%"
|
return "<0,000001%"
|
||||||
return f"{percent:.6f}%".replace(".", ",")
|
return f"{percent:.6f}%".replace(".", ",")
|
||||||
|
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
"""Render the landing screen with lottery simulator and insights."""
|
"""Render the landing screen with lottery simulator and insights."""
|
||||||
host_name = request.get_host().lower()
|
host_name = request.get_host().lower()
|
||||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
|
||||||
lottery_choices = [
|
loterias_db = Lottery.objects.all()
|
||||||
(key, config["label"]) for key, config in LOTTERY_CONFIGS.items()
|
lottery_choices = [(l.name, l.get_name_display()) for l in loterias_db]
|
||||||
]
|
|
||||||
lottery_cards = []
|
lottery_cards = []
|
||||||
for key, config in LOTTERY_CONFIGS.items():
|
for l in loterias_db:
|
||||||
total_combinations = math.comb(config["range_max"], config["picks"])
|
total_combinations = math.comb(l.max_number, l.numbers_to_draw)
|
||||||
lottery_cards.append(
|
lottery_cards.append({
|
||||||
{
|
"key": l.name,
|
||||||
"key": key,
|
"label": l.get_name_display(),
|
||||||
"label": config["label"],
|
"tagline": f"{l.numbers_to_draw} dezenas entre {l.max_number}",
|
||||||
"tagline": config["tagline"],
|
"range_max": l.max_number,
|
||||||
"range_max": config["range_max"],
|
"picks": l.numbers_to_draw,
|
||||||
"picks": config["picks"],
|
"odds": _format_odds(total_combinations),
|
||||||
"odds": _format_odds(total_combinations),
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
form = LotterySimulatorForm(
|
form = LotterySimulatorForm(
|
||||||
request.POST or None,
|
request.POST or None,
|
||||||
@ -131,64 +105,75 @@ def home(request):
|
|||||||
lottery_key = form.cleaned_data["lottery_type"]
|
lottery_key = form.cleaned_data["lottery_type"]
|
||||||
draws_to_consider = form.cleaned_data["draws_to_consider"]
|
draws_to_consider = form.cleaned_data["draws_to_consider"]
|
||||||
games_to_generate = form.cleaned_data["games_to_generate"]
|
games_to_generate = form.cleaned_data["games_to_generate"]
|
||||||
config = LOTTERY_CONFIGS[lottery_key]
|
|
||||||
draws = _generate_sample_draws(config)
|
lottery_obj = Lottery.objects.get(name=lottery_key)
|
||||||
draws_to_consider = min(draws_to_consider, len(draws))
|
annulled = [int(n) for n in lottery_obj.annulled_numbers.split(',') if n]
|
||||||
recent_draws = draws[-draws_to_consider:]
|
|
||||||
frequency = Counter(
|
# Busca sorteios reais no banco
|
||||||
number for draw in recent_draws for number in draw
|
draws_db = DrawResult.objects.filter(lottery=lottery_obj)[:draws_to_consider]
|
||||||
)
|
draw_lists = []
|
||||||
numbers = list(range(1, config["range_max"] + 1))
|
for d in draws_db:
|
||||||
|
draw_lists.append([int(n) for n in d.numbers.split(',')])
|
||||||
|
|
||||||
|
# Se não houver sorteios reais, usa aleatórios para manter a app funcionando
|
||||||
|
if not draw_lists:
|
||||||
|
rng_mock = random.Random(42)
|
||||||
|
population = list(range(1, lottery_obj.max_number + 1))
|
||||||
|
for _ in range(draws_to_consider):
|
||||||
|
draw_lists.append(rng_mock.sample(population, lottery_obj.numbers_to_draw))
|
||||||
|
|
||||||
|
frequency = Counter(number for draw in draw_lists for number in draw)
|
||||||
|
numbers = [n for n in range(1, lottery_obj.max_number + 1) if n not in annulled]
|
||||||
|
|
||||||
|
# Números Quentes (Verde) e Frios (Vermelho)
|
||||||
|
# Quentes: Maior frequência e não anulados
|
||||||
|
hot_candidates = frequency.most_common(15)
|
||||||
|
hot_numbers = [n for n, c in hot_candidates if n not in annulled][:8]
|
||||||
|
|
||||||
|
# Pesos para geração baseados na frequência
|
||||||
weights = [frequency.get(number, 0) + 1 for number in numbers]
|
weights = [frequency.get(number, 0) + 1 for number in numbers]
|
||||||
rng = random.Random(f"{lottery_key}-{draws_to_consider}-{games_to_generate}")
|
rng = random.Random(f"{lottery_key}-{draws_to_consider}-{games_to_generate}")
|
||||||
|
|
||||||
suggestions = []
|
suggestions = []
|
||||||
for _ in range(games_to_generate):
|
for _ in range(games_to_generate):
|
||||||
suggestion = _weighted_unique_sample(
|
if len(numbers) >= lottery_obj.numbers_to_draw:
|
||||||
numbers, weights, config["picks"], rng
|
# Simulação ponderada simples
|
||||||
)
|
indices = list(range(len(numbers)))
|
||||||
suggestions.append(sorted(suggestion))
|
ws = [frequency.get(numbers[i], 0) + 1 for i in indices]
|
||||||
|
selected_indices = random.choices(indices, weights=ws, k=lottery_obj.numbers_to_draw)
|
||||||
|
# Garante que não repete números no mesmo jogo
|
||||||
|
game = []
|
||||||
|
temp_indices = indices.copy()
|
||||||
|
for _p in range(lottery_obj.numbers_to_draw):
|
||||||
|
ws_temp = [frequency.get(numbers[i], 0) + 1 for i in temp_indices]
|
||||||
|
idx = random.choices(range(len(temp_indices)), weights=ws_temp, k=1)[0]
|
||||||
|
game.append(numbers[temp_indices[idx]])
|
||||||
|
del temp_indices[idx]
|
||||||
|
suggestions.append(sorted(game))
|
||||||
|
|
||||||
|
total_combinations = math.comb(lottery_obj.max_number, lottery_obj.numbers_to_draw)
|
||||||
|
|
||||||
total_combinations = math.comb(config["range_max"], config["picks"])
|
|
||||||
hot_numbers = [
|
|
||||||
number for number, _ in frequency.most_common(8)
|
|
||||||
]
|
|
||||||
cold_numbers = [
|
|
||||||
number
|
|
||||||
for number, _ in sorted(
|
|
||||||
frequency.items(), key=lambda item: item[1]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
missing_numbers = [
|
|
||||||
number for number in numbers if number not in frequency
|
|
||||||
]
|
|
||||||
cold_numbers = (missing_numbers + cold_numbers)[:8]
|
|
||||||
result = {
|
result = {
|
||||||
"lottery": config["label"],
|
"lottery": lottery_obj.get_name_display(),
|
||||||
"draws_used": draws_to_consider,
|
"draws_used": len(draw_lists),
|
||||||
"total_combinations": f"{total_combinations:,}".replace(",", "."),
|
"total_combinations": f"{total_combinations:,}".replace(",", "."),
|
||||||
"odds": _format_odds(total_combinations),
|
"odds": _format_odds(total_combinations),
|
||||||
"percent": _format_percent(total_combinations),
|
"percent": _format_percent(total_combinations),
|
||||||
"suggestions": suggestions,
|
"suggestions": suggestions,
|
||||||
"hot_numbers": hot_numbers,
|
"hot_numbers": hot_numbers,
|
||||||
"cold_numbers": cold_numbers,
|
"annulled_numbers": annulled,
|
||||||
}
|
}
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"project_name": "LotoPulse",
|
"project_name": "LotoPulse",
|
||||||
"project_description": (
|
"project_description": "Análise matemática e editor de probabilidades para Loterias Caixa.",
|
||||||
"Analise loterias brasileiras, gere jogos e acompanhe "
|
|
||||||
"probabilidades com base nos sorteios mais recentes."
|
|
||||||
),
|
|
||||||
"agent_brand": agent_brand,
|
"agent_brand": agent_brand,
|
||||||
"django_version": django_version(),
|
"django_version": django_version(),
|
||||||
"python_version": platform.python_version(),
|
"python_version": platform.python_version(),
|
||||||
"current_time": now,
|
"current_time": now,
|
||||||
"host_name": host_name,
|
|
||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
|
||||||
"deployment_timestamp": now.strftime("%Y%m%d%H%M%S"),
|
|
||||||
"form": form,
|
"form": form,
|
||||||
"result": result,
|
"result": result,
|
||||||
"lottery_cards": lottery_cards,
|
"lottery_cards": lottery_cards,
|
||||||
|
"is_admin": check_admin(request),
|
||||||
}
|
}
|
||||||
return render(request, "core/index.html", context)
|
return render(request, "core/index.html", context)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user