Text
Text
DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#5D5CDE">
<title>Simulateur Pro de Matchs de Football</title>
<link rel="manifest" href="#" id="manifestPlaceholder">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?
family=Montserrat:wght@400;500;600;700&family=Roboto:wght@300;400;500;700&display=s
wap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-
awesome/6.4.0/css/all.min.css">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js"></
script>
<style>
:root {
/* Couleurs de base */
--primary: #5D5CDE;
--primary-light: #7675EF;
--primary-dark: #3E3DB0;
--accent: #F39C12;
--accent-light: #F7B541;
--accent-dark: #E67E22;
--success: #2ECC71;
--danger: #E74C3C;
--warning: #F39C12;
--info: #3498DB;
/* Animations */
--transition-speed: 0.3s;
/* Rayons de bordure */
--border-radius-sm: 0.25rem;
--border-radius: 0.5rem;
--border-radius-lg: 1rem;
--border-radius-xl: 1.5rem;
/* Espacement */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
}
/* Thème sombre */
.dark {
--bg-main: #121220;
--bg-card: #1E1E30;
--bg-input: #252538;
--text-primary: #F7F7FC;
--text-secondary: #AEAECE;
--text-muted: #8E8EA9;
--border-color: #323252;
--shadow-color: rgba(0, 0, 0, 0.2);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Roboto', sans-serif;
background-color: var(--bg-main);
color: var(--text-primary);
transition: background-color var(--transition-speed), color var(--
transition-speed);
min-height: 100vh;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 var(--spacing-md);
}
.card {
background-color: var(--bg-card);
border-radius: var(--border-radius-lg);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
box-shadow: 0 4px 16px var(--shadow-color);
border: 1px solid var(--border-color);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px var(--shadow-color);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
border-bottom: 1px solid var(--border-color);
padding-bottom: var(--spacing-md);
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
}
.card-body {
position: relative;
}
/* Formulaires */
.form-group {
margin-bottom: var(--spacing-md);
}
.form-group label {
display: block;
margin-bottom: var(--spacing-xs);
color: var(--text-secondary);
font-weight: 500;
font-size: 0.9rem;
}
.form-control {
width: 100%;
padding: var(--spacing-md);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background-color: var(--bg-input);
color: var(--text-primary);
font-size: 1rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.form-control:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(93, 92, 222, 0.15);
}
.form-control::placeholder {
color: var(--text-muted);
}
select.form-control {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg
xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%238E8EA9'
viewBox='0 0 16 16'%3E%3Cpath d='M7.247 11.14L2.451 5.658C1.885 5.013 2.345 4 3.204
4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 32px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--spacing-md);
}
.form-row {
display: flex;
gap: var(--spacing-md);
}
.form-row > * {
flex: 1;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: var(--spacing-md);
margin-top: var(--spacing-lg);
}
/* Boutons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
padding: var(--spacing-md) var(--spacing-lg);
border-radius: var(--border-radius);
font-weight: 500;
cursor: pointer;
transition: all var(--transition-speed);
text-align: center;
border: none;
font-size: 0.95rem;
}
.btn-primary {
background-color: var(--primary);
color: white;
box-shadow: 0 4px 6px rgba(93, 92, 222, 0.25);
}
.btn-primary:hover {
background-color: var(--primary-light);
box-shadow: 0 6px 8px rgba(93, 92, 222, 0.3);
}
.btn-accent {
background-color: var(--accent);
color: white;
box-shadow: 0 4px 6px rgba(243, 156, 18, 0.25);
}
.btn-accent:hover {
background-color: var(--accent-light);
box-shadow: 0 6px 8px rgba(243, 156, 18, 0.3);
}
.btn-secondary {
background-color: var(--bg-card);
color: var(--text-secondary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background-color: var(--border-color);
color: var(--text-primary);
}
.btn-success {
background-color: var(--success);
color: white;
box-shadow: 0 4px 6px rgba(46, 204, 113, 0.25);
}
.btn-success:hover {
background-color: var(--success);
box-shadow: 0 6px 8px rgba(46, 204, 113, 0.3);
}
.btn-danger {
background-color: var(--danger);
color: white;
box-shadow: 0 4px 6px rgba(231, 76, 60, 0.25);
}
.btn-danger:hover {
background-color: var(--danger);
box-shadow: 0 6px 8px rgba(231, 76, 60, 0.3);
}
.btn-sm {
padding: var(--spacing-sm) var(--spacing-md);
font-size: 0.85rem;
}
.btn-lg {
padding: var(--spacing-lg) var(--spacing-xl);
font-size: 1.1rem;
}
/* Barre de vitesse */
.speed-control {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-md);
}
.speed-label {
font-weight: 500;
color: var(--text-secondary);
}
.speed-options {
display: flex;
gap: var(--spacing-xs);
}
.speed-option {
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--border-radius-sm);
background-color: var(--bg-card);
border: 1px solid var(--border-color);
cursor: pointer;
transition: all var(--transition-speed);
}
.speed-option:hover {
border-color: var(--primary);
color: var(--primary);
}
.speed-option.active {
background-color: var(--primary);
color: white;
border-color: var(--primary);
}
/* Match Style */
.match-field {
background-color: #1b5e20;
border-radius: var(--border-radius);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-lg);
position: relative;
min-height: 200px;
color: white;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
.match-field-lines {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 4px solid rgba(255, 255, 255, 0.3);
pointer-events: none;
}
.match-field-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60px;
height: 60px;
border-radius: 50%;
border: 4px solid rgba(255, 255, 255, 0.3);
pointer-events: none;
}
.match-field-center::before {
content: '';
position: absolute;
top: 50%;
left: -100px;
right: -100px;
height: 4px;
background-color: rgba(255, 255, 255, 0.3);
transform: translateY(-50%);
}
.match-scoreboard {
background-color: var(--bg-card);
border-radius: var(--border-radius);
padding: var(--spacing-lg);
text-align: center;
box-shadow: 0 4px 16px var(--shadow-color);
margin-bottom: var(--spacing-lg);
}
.match-teams {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
}
.match-team {
flex: 1;
text-align: center;
}
.match-score {
font-size: 2.5rem;
font-weight: 700;
padding: 0 var(--spacing-md);
}
.match-time {
font-size: 1.2rem;
font-weight: 600;
color: var(--primary);
margin-bottom: var(--spacing-sm);
}
.match-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-sm);
margin-top: var(--spacing-lg);
padding-top: var(--spacing-md);
border-top: 1px solid var(--border-color);
}
.match-stat {
text-align: center;
}
.stat-label {
font-size: 0.8rem;
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
}
.stat-values {
display: flex;
justify-content: space-between;
align-items: center;
}
.stat-value {
font-weight: 600;
flex: 1;
}
.stat-bar {
height: 6px;
background-color: var(--border-color);
border-radius: 3px;
margin-top: var(--spacing-xs);
position: relative;
overflow: hidden;
}
.stat-fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: var(--primary);
transition: width 0.5s ease-out;
}
/* Match Commentary */
.match-commentary {
background-color: var(--bg-card);
border-radius: var(--border-radius);
padding: var(--spacing-md);
box-shadow: 0 4px 16px var(--shadow-color);
margin-bottom: var(--spacing-lg);
height: 300px;
overflow-y: auto;
}
.commentary-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: var(--spacing-md);
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--border-color);
}
.commentary-list {
list-style: none;
}
.commentary-item {
padding: var(--spacing-sm) 0;
border-bottom: 1px dashed var(--border-color);
}
.commentary-time {
font-weight: 600;
color: var(--primary);
}
/* Badges */
.badge {
display: inline-block;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--border-radius-sm);
font-size: 0.75rem;
font-weight: 500;
}
.badge-primary {
background-color: rgba(93, 92, 222, 0.1);
color: var(--primary);
}
.badge-success {
background-color: rgba(46, 204, 113, 0.1);
color: var(--success);
}
.badge-danger {
background-color: rgba(231, 76, 60, 0.1);
color: var(--danger);
}
.badge-warning {
background-color: rgba(243, 156, 18, 0.1);
color: var(--warning);
}
/* Team Category */
.team-category {
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--border-radius-sm);
font-size: 0.75rem;
font-weight: 500;
display: inline-block;
margin-top: var(--spacing-xs);
}
.category-grande {
background-color: rgba(46, 204, 113, 0.1);
color: var(--success);
}
.category-moyenne {
background-color: rgba(243, 156, 18, 0.1);
color: var(--warning);
}
.category-petite {
background-color: rgba(231, 76, 60, 0.1);
color: var(--danger);
}
/* Team Styles */
.team-style {
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--border-radius-sm);
font-size: 0.75rem;
font-weight: 500;
display: inline-block;
margin-top: var(--spacing-xs);
}
.style-offensive {
background-color: rgba(231, 76, 60, 0.1);
color: var(--danger);
}
.style-balanced {
background-color: rgba(243, 156, 18, 0.1);
color: var(--warning);
}
.style-defensive {
background-color: rgba(46, 204, 113, 0.1);
color: var(--success);
}
/* Toggle */
.toggle-container {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 26px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--border-color);
transition: .4s;
border-radius: 26px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: var(--primary);
}
input:checked + .toggle-slider:before {
transform: translateX(24px);
}
/* Utilitaires */
.mb-1 { margin-bottom: var(--spacing-xs); }
.mb-2 { margin-bottom: var(--spacing-sm); }
.mb-3 { margin-bottom: var(--spacing-md); }
.mb-4 { margin-bottom: var(--spacing-lg); }
.mb-5 { margin-bottom: var(--spacing-xl); }
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(93, 92, 222, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 5px rgba(93, 92, 222, 0);
}
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(93, 92, 222, 0);
}
}
/* Goal Animation */
.goal-animation {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
animation: goalFadeIn 0.3s forwards;
}
.goal-content {
padding: var(--spacing-xl);
background-color: rgba(93, 92, 222, 0.9);
border-radius: var(--border-radius-lg);
text-align: center;
color: white;
transform: scale(0.8);
animation: goalScale 0.5s forwards;
}
.goal-text {
font-size: 3rem;
font-weight: 700;
margin-bottom: var(--spacing-lg);
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
}
.goal-team {
font-size: 1.5rem;
font-weight: 600;
}
@keyframes goalFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes goalScale {
from { transform: scale(0.8); }
to { transform: scale(1); }
}
/* Notifications */
#notifications {
position: fixed;
top: var(--spacing-lg);
right: var(--spacing-lg);
z-index: 1000;
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
max-width: 350px;
}
.notification {
display: flex;
align-items: center;
gap: var(--spacing-md);
padding: var(--spacing-md);
background-color: var(--bg-card);
border-radius: var(--border-radius);
box-shadow: 0 4px 12px var(--shadow-color);
animation: slideIn 0.3s ease;
border-left: 4px solid var(--primary);
}
.notification.success {
border-color: var(--success);
}
.notification.error {
border-color: var(--danger);
}
.notification.warning {
border-color: var(--warning);
}
.notification.info {
border-color: var(--primary);
}
.notification i {
font-size: 1.2rem;
}
.notification.success i {
color: var(--success);
}
.notification.error i {
color: var(--danger);
}
.notification.warning i {
color: var(--warning);
}
.notification.info i {
color: var(--primary);
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeout {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(30px);
}
}
/* Match History */
.match-history-container {
margin-top: var(--spacing-xl);
}
.match-history-list {
max-height: 500px;
overflow-y: auto;
}
.match-history-item {
background-color: var(--bg-card);
border-radius: var(--border-radius);
padding: var(--spacing-md);
margin-bottom: var(--spacing-sm);
border: 1px solid var(--border-color);
transition: transform 0.2s ease;
}
.match-history-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--shadow-color);
}
.match-result {
display: flex;
justify-content: space-between;
align-items: center;
}
.match-details {
padding-top: var(--spacing-sm);
margin-top: var(--spacing-sm);
border-top: 1px solid var(--border-color);
font-size: 0.9rem;
color: var(--text-secondary);
}
/* Switch sombre/clair */
.theme-switch {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 100;
}
.theme-switch-button {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--bg-card);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 8px var(--shadow-color);
cursor: pointer;
border: none;
color: var(--text-primary);
}
.import-data-card textarea {
min-height: 120px;
resize: vertical;
}
/* Scheduled Matches */
.scheduled-matches {
margin-top: var(--spacing-lg);
}
.scheduled-match-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-md);
background-color: var(--bg-card);
border-radius: var(--border-radius);
margin-bottom: var(--spacing-sm);
border: 1px solid var(--border-color);
}
.scheduled-match-teams {
font-weight: 600;
}
.scheduled-match-time {
color: var(--text-secondary);
font-size: 0.9rem;
}
.team-selector .selected-team {
display: flex;
align-items: center;
padding: var(--spacing-sm);
border: 1px solid var(--border-color);
background-color: var(--bg-input);
border-radius: var(--border-radius);
gap: var(--spacing-sm);
cursor: pointer;
}
.team-selector .team-list {
position: absolute;
top: 100%;
left: 0;
right: 0;
max-height: 250px;
overflow-y: auto;
background-color: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
z-index: 10;
box-shadow: 0 4px 12px var(--shadow-color);
display: none;
}
.team-selector .team-item {
padding: var(--spacing-sm);
cursor: pointer;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
}
.team-selector .team-item:hover {
background-color: rgba(93, 92, 222, 0.1);
}
.team-league-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
font-size: 0.75rem;
font-weight: 600;
margin-right: var(--spacing-sm);
}
.team-league-badge.L1 {
background-color: #0053a0;
color: white;
}
.team-league-badge.PL {
background-color: #3d185c;
color: white;
}
.team-league-badge.LL {
background-color: #ee8707;
color: white;
}
.team-league-badge.BL {
background-color: #d3010c;
color: white;
}
.team-league-badge.SA {
background-color: #008756;
color: white;
}
/* Onglets */
.tabs {
display: flex;
gap: 2px;
margin-bottom: var(--spacing-md);
border-bottom: 1px solid var(--border-color);
}
.tab {
padding: var(--spacing-sm) var(--spacing-lg);
cursor: pointer;
background-color: transparent;
border: none;
color: var(--text-secondary);
border-bottom: 2px solid transparent;
transition: all 0.2s ease;
font-weight: 500;
}
.tab:hover {
color: var(--primary);
}
.tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s ease;
}
.datetime-picker input {
flex: 1;
}
.scheduled-matches-list {
max-height: 300px;
overflow-y: auto;
}
.scheduled-match-buttons {
display: flex;
gap: var(--spacing-sm);
}
/* Loader */
.loader-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loader {
width: 50px;
height: 50px;
border: 5px solid var(--bg-card);
border-radius: 50%;
border-top-color: var(--primary);
animation: spin 1s linear infinite;
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}
.card {
padding: var(--spacing-md);
}
.match-scoreboard {
padding: var(--spacing-md);
}
.match-score {
font-size: 2rem;
}
.match-stats {
grid-template-columns: 1fr 1fr;
gap: var(--spacing-md);
}
.form-grid {
grid-template-columns: 1fr;
}
.form-row {
flex-direction: column;
}
.btn {
padding: var(--spacing-sm) var(--spacing-md);
}
.tabs {
overflow-x: auto;
padding-bottom: var(--spacing-xs);
}
.tab {
padding: var(--spacing-sm) var(--spacing-md);
white-space: nowrap;
}
}
.match-stats {
grid-template-columns: 1fr;
}
.datetime-picker {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container mt-4 mb-5">
<header class="text-center mb-5">
<h1 class="text-2xl font-bold mb-2">Simulateur Pro de Matchs de
Football</h1>
<p class="text-muted">Simulez des matchs en temps réel avec analyses
statistiques avancées</p>
</header>
<div class="tabs">
<button class="tab active" data-tab="import-data">Importer
Données</button>
<button class="tab" data-tab="match-setup">Configuration Match</button>
<button class="tab" data-tab="scheduled-matches">Matchs
Planifiés</button>
<button class="tab" data-tab="match-history">Historique</button>
</div>
<div class="form-group">
<label for="dataInput">Données d'équipes</label>
<textarea id="dataInput" class="form-control"
placeholder="Collez vos données (format avec tabs/tabulations)"></textarea>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">Équipes importées</h2>
</div>
<div class="card-body">
<div class="form-group">
<input type="text" id="teamSearch" class="form-control"
placeholder="Rechercher une équipe...">
</div>
<div class="form-group">
<label for="team2">Équipe Extérieur</label>
<select id="team2" class="form-control" required>
<option value="">Sélectionner une
équipe</option>
<!-- Options générées dynamiquement -->
</select>
<div class="flex gap-2 mt-2">
<span id="team2Category" class="team-
category"></span>
<span id="team2Style"
class="team-style"></span>
</div>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label for="matchType">Type de Match</label>
<select id="matchType" class="form-control"
required>
<option
value="Championnat">Championnat</option>
<option value="Coupe">Coupe</option>
<option value="Derby">Derby</option>
<option value="Amical">Amical</option>
</select>
</div>
<div class="form-group">
<label for="venue">Stade</label>
<select id="venue" class="form-control">
<option value="home">Domicile</option>
<option value="away">Extérieur</option>
<option value="neutral">Terrain neutre</option>
</select>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label for="weather">Conditions Météo</label>
<select id="weather" class="form-control">
<option value="clear">Ensoleillé</option>
<option value="cloudy">Nuageux</option>
<option value="rainy">Pluvieux</option>
<option value="snowy">Neigeux</option>
<option value="foggy">Brumeux</option>
</select>
</div>
<div class="form-group">
<label for="crowd">Affluence</label>
<select id="crowd" class="form-control">
<option value="full">Complet</option>
<option value="high">Élevée</option>
<option value="medium"
selected>Moyenne</option>
<option value="low">Faible</option>
<option value="empty">Huis clos</option>
</select>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label for="pastMatches">Confrontations directes
(optionnel)</label>
<input type="text" id="pastMatches" class="form-
control" placeholder="Ex: 2-1,0-0,3-2">
<div class="text-sm text-muted mt-1">Format:
résultats séparés par virgules</div>
</div>
<div class="form-group">
<label>Forme récente (optionnel)</label>
<div class="flex gap-2">
<input type="text" id="last5Team1" class="form-
control" placeholder="Équipe 1: V,N,D,V,V">
<input type="text" id="last5Team2" class="form-
control" placeholder="Équipe 2: D,V,V,N,D">
</div>
<div class="text-sm text-muted mt-1">Format: V
(victoire), N (nul), D (défaite)</div>
</div>
</div>
<div class="toggle-container">
<label class="toggle-switch">
<input type="checkbox" id="advancedStats"
checked>
<span class="toggle-slider"></span>
</label>
<span>Statistiques avancées</span>
</div>
<div class="toggle-container">
<label class="toggle-switch">
<input type="checkbox" id="realismMode"
checked>
<span class="toggle-slider"></span>
</label>
<span>Mode réalisme</span>
</div>
<div class="toggle-container">
<label class="toggle-switch">
<input type="checkbox"
id="backgroundNotifications" checked>
<span class="toggle-slider"></span>
</label>
<span>Notifications en arrière-plan</span>
</div>
<div class="toggle-container">
<label class="toggle-switch">
<input type="checkbox"
id="intensityFactor">
<span class="toggle-slider"></span>
</label>
<span>Facteur d'intensité aléatoire</span>
</div>
<div class="toggle-container">
<label class="toggle-switch">
<input type="checkbox"
id="scheduleForLater">
<span class="toggle-slider"></span>
</label>
<span>Planifier pour plus tard</span>
</div>
</div>
</div>
<div class="form-actions">
<button type="button" id="resetBtn" class="btn btn-
secondary">
<i class="fas fa-undo"></i> Réinitialiser
</button>
<button type="button" id="startSimulationBtn"
class="btn btn-primary">
<i class="fas fa-play"></i> Lancer la simulation
</button>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Statistiques du match</h3>
</div>
<div class="card-body">
<div class="match-stats">
<div class="match-stat">
<div class="stat-label">Possession</div>
<div class="stat-values">
<div class="stat-value"
id="homePossession">50%</div>
<div class="stat-value"
id="awayPossession">50%</div>
</div>
<div class="stat-bar">
<div class="stat-fill" id="possessionBar"
style="width: 50%;"></div>
</div>
</div>
<div class="match-stat">
<div class="stat-label">Tirs</div>
<div class="stat-values">
<div class="stat-value"
id="homeShots">0</div>
<div class="stat-value"
id="awayShots">0</div>
</div>
<div class="stat-bar">
<div class="stat-fill" id="shotsBar"
style="width: 50%;"></div>
</div>
</div>
<div class="match-stat">
<div class="stat-label">Tirs cadrés</div>
<div class="stat-values">
<div class="stat-value"
id="homeShotsOnTarget">0</div>
<div class="stat-value"
id="awayShotsOnTarget">0</div>
</div>
<div class="stat-bar">
<div class="stat-fill"
id="shotsOnTargetBar" style="width: 50%;"></div>
</div>
</div>
<div class="match-stat">
<div class="stat-label">Corners</div>
<div class="stat-values">
<div class="stat-value"
id="homeCorners">0</div>
<div class="stat-value"
id="awayCorners">0</div>
</div>
<div class="stat-bar">
<div class="stat-fill" id="cornersBar"
style="width: 50%;"></div>
</div>
</div>
<div class="match-stat">
<div class="stat-label">Cartons jaunes</div>
<div class="stat-values">
<div class="stat-value"
id="homeYellowCards">0</div>
<div class="stat-value"
id="awayYellowCards">0</div>
</div>
</div>
<div class="match-stat">
<div class="stat-label">Cartons rouges</div>
<div class="stat-values">
<div class="stat-value"
id="homeRedCards">0</div>
<div class="stat-value"
id="awayRedCards">0</div>
</div>
</div>
</div>
<div class="match-stat">
<div class="stat-label">Attaques
dangereuses</div>
<div class="stat-values">
<div class="stat-value"
id="homeDangerousAttacks">0</div>
<div class="stat-value"
id="awayDangerousAttacks">0</div>
</div>
<div class="stat-bar">
<div class="stat-fill"
id="dangerousAttacksBar" style="width: 50%;"></div>
</div>
</div>
<div class="match-stat">
<div class="stat-label">Momentum</div>
<div class="stat-values">
<div class="stat-value"
id="homeMomentum">0</div>
<div class="stat-value"
id="awayMomentum">0</div>
</div>
<div class="stat-bar">
<div class="stat-fill" id="momentumBar"
style="width: 50%;"></div>
</div>
</div>
<div class="match-stat">
<div class="stat-label">Fatigue</div>
<div class="stat-values">
<div class="stat-value"
id="homeFatigue">100%</div>
<div class="stat-value"
id="awayFatigue">100%</div>
</div>
<div class="stat-bar">
<div class="stat-fill" id="fatigueBar"
style="width: 50%;"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-between mt-4">
<button id="pauseResumeBtn" class="btn btn-secondary">
<i class="fas fa-pause"></i> Pause
</button>
<button id="stopSimulationBtn" class="btn btn-danger">
<i class="fas fa-stop"></i> Arrêter
</button>
</div>
</div>
<script>
/* =========================================================
INITIALISATION ET GESTION DES DONNÉES
========================================================= */
// Variables globales
let teamData = {}; // Stockage des données d'équipes
let matchHistory = []; // Historique des matchs
let currentMatch = null; // Match en cours de simulation
let simulationTimer = null; // Timer pour la simulation
let simulationPaused = false; // État de pause
let scheduledMatches = []; // Matchs planifiés
let scheduledMatchesTimers = {}; // Timers pour les matchs planifiés
// Programmer la notification
if (delay > 0) {
setTimeout(() => {
self.registration.showNotification(title, {
body,
icon,
badge: icon,
tag: id,
renotify: true,
data: { id }
});
}, delay);
}
} else if (event.data && event.data.type === 'SHOW_NOTIFICATION') {
const {id, title, body, icon} = event.data.payload;
// Ouvrir/activer l'application
event.waitUntil(
clients.matchAll({type: 'window'}).then(clientList => {
for (const client of clientList) {
if ('focus' in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow('./');
}
})
);
});
`;
// Initialiser l'application
document.addEventListener('DOMContentLoaded', function() {
// Demander la permission pour les notifications
if ('Notification' in window) {
if (Notification.permission !== 'granted' &&
Notification.permission !== 'denied') {
Notification.requestPermission();
}
}
// Vérifier le thème
checkTheme();
// Format YYYY-MM-DDThh:mm
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
document.getElementById('scheduleDateTime').value = `${year}-$
{month}-${day}T${hours}:${minutes}`;
}
});
function initTabs() {
const tabs = document.querySelectorAll('.tab');
tabs.forEach(tab => {
tab.addEventListener('click', function() {
const tabId = this.getAttribute('data-tab');
document.getElementById('dataLoadedIndicator').style.display = 'block';
}
}
const storedMatchHistory =
localStorage.getItem('simulatorMatchHistory');
if (storedMatchHistory) {
matchHistory = JSON.parse(storedMatchHistory);
updateMatchHistory();
}
const storedScheduledMatches =
localStorage.getItem('simulatorScheduledMatches');
if (storedScheduledMatches) {
scheduledMatches = JSON.parse(storedScheduledMatches);
updateScheduledMatchesDisplay();
}
} catch (error) {
console.error('Erreur lors du chargement des données:', error);
showNotification('Erreur lors du chargement des données', 'error');
}
}
if (team2) {
const team2Category = classifyTeam(team2);
const team2Style = identifyTeamStyle(team2);
const categoryElement = document.getElementById('team2Category');
categoryElement.textContent = team2Category;
categoryElement.className = `team-category category-$
{team2Category.toLowerCase()}`;
// Réinitialiser le formulaire
function resetForm() {
document.getElementById('matchSetupForm').reset();
document.getElementById('team1Category').textContent = '';
document.getElementById('team1Style').textContent = '';
document.getElementById('team2Category').textContent = '';
document.getElementById('team2Style').textContent = '';
document.getElementById('startSimulationBtn').disabled = false;
document.getElementById('scheduleDatetimeContainer').style.display =
'none';
}
if (teams.length === 0) {
teamsContainer.innerHTML = `
<div class="text-center text-muted py-4">
<i class="fas fa-database fa-2x mb-2"></i>
<p>Aucune équipe importée</p>
<p class="text-sm">Importez des données pour commencer</p>
</div>
`;
return;
}
if (filteredTeams.length === 0) {
teamsContainer.innerHTML = `
<div class="text-center text-muted py-4">
<i class="fas fa-search fa-2x mb-2"></i>
<p>Aucune équipe ne correspond à votre recherche</p>
<p class="text-sm">Essayez un autre terme de recherche</p>
</div>
`;
return;
}
html += `
<div class="p-3 bg-card rounded shadow-sm border border-color
hover:shadow-md transition-all">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center">
${leagueBadgeHtml}
<span class="font-bold">${team}</span>
</div>
<div class="flex gap-1">
<span class="team-category category-$
{category.toLowerCase()}">${category}</span>
<span class="team-style style-$
{style.toLowerCase()}">${style}</span>
</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm">
<div>
<span class="text-muted">xG/90:</span>
<span class="font-bold">${(data["xG/90"] ||
0).toFixed(2)}</span>
</div>
<div>
<span class="text-muted">Poss:</span>
<span class="font-bold">${data["Poss"] ||
0}%</span>
</div>
<div>
<span class="text-muted">Âge:</span>
<span class="font-bold">${data["Age"] || 0}</span>
</div>
<div>
<span class="text-muted">Discipline:</span>
<span class="font-bold">$
{evaluateDiscipline(team)}/5</span>
</div>
</div>
</div>
`;
});
teamsContainer.innerHTML = html;
}
teamData = demoTeams;
populateTeamOptions();
updateTeamsContainer();
saveData();
/* =========================================================
LOGIQUE DE SIMULATION DE MATCH
========================================================= */
if (possession > 55 && xG > 2.0 && GA < 1.2) return "Grande";
if (possession < 45 && xG < 1.2 && GA > 1.8) return "Petite";
return "Moyenne";
}
if (!data || !opponentData) {
console.error('Données d\'équipe manquantes pour le calcul xG');
return 0.8; // Valeur par défaut
}
// Ajustements supplémentaires
const teamCategory = classifyTeam(team);
return adjusted_xG;
}
let stats = {
matchCount: pastMatches.length,
team1Wins: 0,
team2Wins: 0,
draws: 0,
team1Goals: 0,
team2Goals: 0,
over25: 0,
under25: 0,
btts: 0,
avgTeam1Goals: 0,
avgTeam2Goals: 0
};
pastMatches.forEach(score => {
let [goals1, goals2] = score.split("-").map(Number);
if (isNaN(goals1) || isNaN(goals2)) return;
stats.team1Goals += goals1;
stats.team2Goals += goals2;
return stats;
}
results.forEach(result => {
if (result === "V") win++;
else if (result === "N") draw++;
else if (result === "D") loss++;
});
return {
results: results,
winRate: (win / totalMatches) * 100,
drawRate: (draw / totalMatches) * 100,
lossRate: (loss / totalMatches) * 100
};
}
do {
k++;
p *= Math.random();
} while (p > L);
return k - 1;
}
// --------------------------------
// FONCTIONS DE SIMULATION DE MATCH
// --------------------------------
// Force relative des équipes (entre 0.7 et 1.3) basée sur la catégorie
let team1StrengthFactor = 1.0;
let team2StrengthFactor = 1.0;
// Paramètres supplémentaires
venue: venue,
weather: weather,
crowd: crowd,
// Options avancées
commentaryEnabled: commentaryEnabled,
advancedStats: advancedStats,
realismMode: realismMode,
intensityFactor: intensityFactor,
backgroundNotifications: backgroundNotifications,
// Données additionnelles
formTeam1: formTeam1,
formTeam2: formTeam2,
h2hStats: h2hStats,
pastMatches: pastMatches,
// Données statistiques
team1Stats: Object.assign({}, team1Stats),
team2Stats: Object.assign({}, team2Stats),
// Facteurs de force
team1StrengthFactor: team1StrengthFactor,
team2StrengthFactor: team2StrengthFactor,
expectedStats: {
shots: { team1: expectedShotsTeam1, team2:
expectedShotsTeam2 },
corners: { team1: expectedCornersTeam1, team2:
expectedCornersTeam2 }
},
// Données de match
score1: 0,
score2: 0,
minute: 0,
status: "En attente",
possessionTeam: dominationBase > 50 ? team1 : team2,
intensity: 1.0,
team1Style: team1Style,
team2Style: team2Style,
team1Discipline: team1Discipline,
team2Discipline: team2Discipline,
// Facteurs dynamiques
fatigue: {
team1: 1.0,
team2: 1.0
},
momentum: {
team1: 0,
team2: 0
},
// Historique d'événements
events: [],
displayedEvents: [],
// Statistiques de match
stats: {
yellowCards: {
team1: 0,
team2: 0
},
redCards: {
team1: 0,
team2: 0
},
dangerousAttacks: {
team1: 0,
team2: 0
},
shotsOnTarget: {
team1: 0,
team2: 0
},
shots: {
team1: 0,
team2: 0
},
corners: {
team1: 0,
team2: 0
},
possession: {
team1: team1Possession,
team2: team2Possession,
current: dominationBase
},
dominationIndex: dominationBase
},
// Timing
startTime: new Date(),
endTime: null
};
}
// Validation
if (!team1 || !team2) {
showNotification('Veuillez sélectionner les deux équipes',
'warning');
return;
}
if (!scheduledDateTime) {
showNotification('Veuillez spécifier une date et heure pour le
match planifié', 'warning');
return;
}
// Convertir en timestamp
const scheduledTimestamp = new Date(scheduledDateTime).getTime();
const now = Date.now();
// Planifier la notification
scheduleMatchNotification(scheduledMatch);
// Réinitialiser le formulaire
resetForm();
// Initialiser la simulation
currentMatch = initializeMatchSimulation(team1, team2, matchType);
if (!currentMatch) {
showNotification('Erreur lors de l\'initialisation de la
simulation', 'error');
return;
}
// Démarrer la simulation
currentMatch.status = "En cours";
simulationPaused = false;
// Notification de mi-temps
showInAppNotification(` Mi-temps : ${currentMatch.team1} $
{currentMatch.score1}-${currentMatch.score2} ${currentMatch.team2}`, 'info', true);
// Notification de reprise
showInAppNotification(`🏁 2ème mi-temps : $
{currentMatch.team1} ${currentMatch.score1}-${currentMatch.score2} $
{currentMatch.team2}`, 'info', currentMatch.backgroundNotifications);
simulateMatchEvents();
updateMatchDisplay();
// Continuer la simulation
simulationTimer = setTimeout(simulateMatchMinutes,
CURRENT_SIMULATION_SPEED);
}
}, 3000); // Pause de 3 secondes pour la mi-temps
return;
}
// Tir cadré?
if (Math.random() < 0.6) { // 60% des tirs sont cadrés
currentMatch.stats.shotsOnTarget.team1++;
const shotOnTargetComments = [
`🎯 Tir cadré de ${currentMatch.team1}!`,
`🎯 Belle tentative de ${currentMatch.team1}!`,
`🎯 ${currentMatch.team1} teste le gardien adverse.`,
`🎯 Tir puissant de ${currentMatch.team1}!`
];
// Notification de but
const score = `${currentMatch.team1} $
{currentMatch.score1}-${currentMatch.score2} ${currentMatch.team2}`;
showInAppNotification(`⚽ BUT! ${currentMatch.team1} marque!
(${score})`, 'success', true);
// Tir cadré?
if (Math.random() < 0.6) { // 60% des tirs sont cadrés
currentMatch.stats.shotsOnTarget.team2++;
const shotOnTargetComments = [
`🎯 Tir cadré de ${currentMatch.team2}!`,
`🎯 Belle tentative de ${currentMatch.team2}!`,
`🎯 ${currentMatch.team2} teste le gardien adverse.`,
`🎯 Tir puissant de ${currentMatch.team2}!`
];
addMatchEvent(currentMatch, goalComment);
// Notification de but
const score = `${currentMatch.team1} $
{currentMatch.score1}-${currentMatch.score2} ${currentMatch.team2}`;
showInAppNotification(`⚽ BUT! ${currentMatch.team2} marque!
(${score})`, 'success', true);
// Probabilités de base
const baseYellowProb = 0.01;
const baseRedProb = 0.001;
// Probabilités finales
let team1YellowProb = baseYellowProb * team1DisciplineFactor *
team1FatigueFactor *
intensityFactor * temporalFactor * scoreDiffFactor
* derbyFactor;
let team2YellowProb = baseYellowProb * team2DisciplineFactor *
team2FatigueFactor *
intensityFactor * temporalFactor * scoreDiffFactor
* derbyFactor;
// Simulation
if (Math.random() < team1YellowProb &&
currentMatch.stats.yellowCards.team1 < maxYellowsPerTeam) {
currentMatch.stats.yellowCards.team1++;
const cardComments = [
`🟨 Carton jaune pour ${currentMatch.team1}!`,
`🟨 Le joueur de ${currentMatch.team1} est averti par
l'arbitre.`,
`🟨 Faute dangereuse, carton jaune pour ${currentMatch.team1}.`
];
const cardComments = [
`🟨 Carton jaune pour ${currentMatch.team2}!`,
`🟨 Le joueur de ${currentMatch.team2} est averti par
l'arbitre.`,
`🟨 Faute dangereuse, carton jaune pour ${currentMatch.team2}.`
];
const cardComments = [
`🟥 Carton rouge! ${currentMatch.team1} joue à ${10 -
currentMatch.stats.redCards.team1}!`,
`🟥 Expulsion pour ${currentMatch.team1}! Faute grave.`,
`🟥 ${currentMatch.team1} perd un joueur sur carton rouge!`
];
const cardComments = [
`🟥 Carton rouge! ${currentMatch.team2} joue à ${10 -
currentMatch.stats.redCards.team2}!`,
`🟥 Expulsion pour ${currentMatch.team2}! Faute grave.`,
`🟥 ${currentMatch.team2} perd un joueur sur carton rouge!`
];
// Facteur de forme
const formFactor = possessionTeam === currentMatch.team1 ?
(currentMatch.formTeam1 ? (currentMatch.formTeam1.winRate / 100) *
0.5 + 0.75 : 1) :
(currentMatch.formTeam2 ? (currentMatch.formTeam2.winRate / 100) *
0.5 + 0.75 : 1);
const shotComments = [
`Tir non cadré de ${possessionTeam}!`,
`${possessionTeam} tente sa chance, mais ça passe à côté.`,
`Tentative manquée pour ${possessionTeam}.`,
`Le tir de ${possessionTeam} n'inquiète pas le gardien adverse.`
];
const cornerComments = [
`${possessionTeam} obtient un corner!`,
`Corner pour ${possessionTeam}.`,
`${possessionTeam} va tirer un corner.`,
`L'arbitre accorde un corner à ${possessionTeam}.`
];
addMatchEvent(currentMatch,
shotComments[Math.floor(Math.random() * shotComments.length)]);
if (currentMatch.stats.redCards.team2 > 0) {
currentMatch.fatigue.team2 = Math.max(0.6,
currentMatch.fatigue.team2 - 0.002 * currentMatch.stats.redCards.team2);
}
}
// 5. Impact de la fatigue
const fatigueFactor = (currentMatch.fatigue.team1 -
currentMatch.fatigue.team2) * 0.1;
// Ajouter à l'historique
match.events.push(event);
// Terminer le match
function finishMatch() {
if (!currentMatch) return;
// Stopper le timer
clearTimeout(simulationTimer);
document.getElementById('matchInfo').textContent = matchInfoText;
// Tirs
document.getElementById('homeShots').textContent =
currentMatch.stats.shots.team1;
document.getElementById('awayShots').textContent =
currentMatch.stats.shots.team2;
// Corners
document.getElementById('homeCorners').textContent =
currentMatch.stats.corners.team1;
document.getElementById('awayCorners').textContent =
currentMatch.stats.corners.team2;
// Cartons
document.getElementById('homeYellowCards').textContent =
currentMatch.stats.yellowCards.team1;
document.getElementById('awayYellowCards').textContent =
currentMatch.stats.yellowCards.team2;
document.getElementById('homeRedCards').textContent =
currentMatch.stats.redCards.team1;
document.getElementById('awayRedCards').textContent =
currentMatch.stats.redCards.team2;
// xG
document.getElementById('homeXG').textContent =
currentMatch.xgTeam1.toFixed(2);
document.getElementById('awayXG').textContent =
currentMatch.xgTeam2.toFixed(2);
// Attaques dangereuses
document.getElementById('homeDangerousAttacks').textContent =
currentMatch.stats.dangerousAttacks.team1;
document.getElementById('awayDangerousAttacks').textContent =
currentMatch.stats.dangerousAttacks.team2;
const totalDangerousAttacks =
currentMatch.stats.dangerousAttacks.team1 +
currentMatch.stats.dangerousAttacks.team2;
const dangerousAttacksPct = totalDangerousAttacks === 0 ? 50 :
Math.round((currentMatch.stats.dangerousAttacks.team1 /
totalDangerousAttacks) * 100);
document.getElementById('dangerousAttacksBar').style.width =
dangerousAttacksPct + "%";
// Momentum
document.getElementById('homeMomentum').textContent =
currentMatch.momentum.team1.toFixed(1);
document.getElementById('awayMomentum').textContent =
currentMatch.momentum.team2.toFixed(1);
// Fatigue
const homeFatigue = Math.round(currentMatch.fatigue.team1 * 100);
const awayFatigue = Math.round(currentMatch.fatigue.team2 * 100);
document.getElementById('homeFatigue').textContent = homeFatigue +
"%";
document.getElementById('awayFatigue').textContent = awayFatigue +
"%";
<div class="text-right">$
{currentMatch.stats.shotsOnTarget.team1}</div>
<div class="text-center">Tirs cadrés</div>
<div>${currentMatch.stats.shotsOnTarget.team2}</div>
<div class="text-right">$
{currentMatch.stats.corners.team1}</div>
<div class="text-center">Corners</div>
<div>${currentMatch.stats.corners.team2}</div>
<div class="text-right">$
{Math.round(currentMatch.stats.possession.current)}%</div>
<div class="text-center">Possession</div>
<div>${100 -
Math.round(currentMatch.stats.possession.current)}%</div>
<div class="text-right">$
{currentMatch.stats.yellowCards.team1}</div>
<div class="text-center">Cartons jaunes</div>
<div>${currentMatch.stats.yellowCards.team2}</div>
<div class="text-right">$
{currentMatch.stats.redCards.team1}</div>
<div class="text-center">Cartons rouges</div>
<div>${currentMatch.stats.redCards.team2}</div>
<div class="text-right">$
{currentMatch.xgTeam1.toFixed(2)}</div>
<div class="text-center">xG</div>
<div>${currentMatch.xgTeam2.toFixed(2)}</div>
</div>
</div>
<div class="card">
<h4 class="font-bold mb-2">Moments clés</h4>
<ul class="text-sm space-y-1" style="max-height: 200px;
overflow-y: auto;">
`;
summaryHTML += `
</ul>
</div>
</div>
<div class="card">
<h4 class="font-bold mb-2">Analyse du match</h4>
<p>${generateMatchAnalysis()}</p>
</div>
`;
// Résultat du match
if (currentMatch.score1 > currentMatch.score2) {
analysis += `${currentMatch.team1} s'impose contre $
{currentMatch.team2} avec un score de ${currentMatch.score1}-$
{currentMatch.score2}. `;
} else if (currentMatch.score1 < currentMatch.score2) {
analysis += `${currentMatch.team2} l'emporte face à $
{currentMatch.team1} avec un score de ${currentMatch.score2}-$
{currentMatch.score1}. `;
} else {
analysis += `Match nul ${currentMatch.score1}-$
{currentMatch.score2} entre ${currentMatch.team1} et ${currentMatch.team2}. `;
}
// Possession
const homePossession =
Math.round(currentMatch.stats.possession.current);
if (homePossession > 55) {
analysis += `${currentMatch.team1} a dominé la possession avec $
{homePossession}% du temps de jeu. `;
} else if (homePossession < 45) {
analysis += `${currentMatch.team2} a dominé la possession avec
${100-homePossession}% du temps de jeu. `;
} else {
analysis += `La possession a été équilibrée (${homePossession}%-
${100-homePossession}%). `;
}
// Occasions
const homeShotsOnTarget = currentMatch.stats.shotsOnTarget.team1;
const awayShotsOnTarget = currentMatch.stats.shotsOnTarget.team2;
const totalShots = currentMatch.stats.shots.team1 +
currentMatch.stats.shots.team2;
if (totalShots > 20) {
analysis += "Match très offensif avec de nombreuses occasions. ";
} else if (totalShots < 10) {
analysis += "Match fermé avec peu d'occasions franches. ";
}
// Cartons
const totalCards = currentMatch.stats.yellowCards.team1 +
currentMatch.stats.yellowCards.team2 +
currentMatch.stats.redCards.team1 +
currentMatch.stats.redCards.team2;
if (totalCards >= 6) {
analysis += "Match tendu avec de nombreux cartons. ";
}
if (currentMatch.stats.redCards.team1 > 0 ||
currentMatch.stats.redCards.team2 > 0) {
const teamWithRed = currentMatch.stats.redCards.team1 > 0 ?
currentMatch.team1 : currentMatch.team2;
analysis += `${teamWithRed} a terminé en infériorité numérique. `;
}
// Efficacité
const team1Efficiency = currentMatch.stats.shotsOnTarget.team1 > 0 ?
currentMatch.score1 /
currentMatch.stats.shotsOnTarget.team1 : 0;
const team2Efficiency = currentMatch.stats.shotsOnTarget.team2 > 0 ?
currentMatch.score2 /
currentMatch.stats.shotsOnTarget.team2 : 0;
// Conclusion
if (currentMatch.score1 + currentMatch.score2 >= 4) {
analysis += "Un match spectaculaire avec beaucoup de buts!";
} else if (currentMatch.score1 + currentMatch.score2 <= 1) {
analysis += "Un match plutôt fermé au niveau du score.";
} else if (Math.abs(currentMatch.score1 - currentMatch.score2) >= 3) {
const dominantTeam = currentMatch.score1 > currentMatch.score2 ?
currentMatch.team1 : currentMatch.team2;
analysis += `Victoire écrasante de ${dominantTeam}.`;
} else if (currentMatch.score1 === currentMatch.score2) {
analysis += "Un match équilibré qui s'est soldé par un nul.";
} else {
analysis += "Un match disputé jusqu'au bout.";
}
return analysis;
}
// Sauvegarder l'historique
saveData();
if (!historyList) return;
if (matchHistory.length === 0) {
historyList.innerHTML = `
<div class="text-center text-muted py-4">
<i class="fas fa-history fa-2x mb-2"></i>
<p>Aucun match dans l'historique</p>
<p class="text-sm">Lancez une simulation pour commencer</p>
</div>
`;
return;
}
matchHistory.forEach(match => {
// Déterminer le style en fonction du résultat
let resultStyle = '';
let resultText = '';
historyHTML += `
<div class="match-history-item" data-match-id="${match.id}">
<div class="match-result">
<div class="flex items-center gap-2">
<span class="badge badge-primary">$
{match.matchType}</span>
<span class="font-bold">${match.team1} $
{match.score1} - ${match.score2} ${match.team2}</span>
</div>
<span class="${resultStyle}
font-bold">${resultText}</span>
</div>
<div class="match-details">
<div class="flex justify-between">
<span>Possession: $
{Math.round(match.stats.possession)}% - ${100-Math.round(match.stats.possession)}
%</span>
<span>xG: ${match.xg.team1.toFixed(2)} - $
{match.xg.team2.toFixed(2)}</span>
<span>Date: ${new
Date(match.date).toLocaleDateString()}</span>
</div>
</div>
</div>
`;
});
historyList.innerHTML = historyHTML;
if (!match) {
showNotification('Match non trouvé dans l\'historique', 'error');
return;
}
<div class="text-right">$
{match.stats.corners.team1}</div>
<div class="text-center">Corners</div>
<div>${match.stats.corners.team2}</div>
<div class="text-right">$
{Math.round(match.stats.possession)}%</div>
<div class="text-center">Possession</div>
<div>${100-Math.round(match.stats.possession)}
%</div>
<div class="text-right">$
{match.stats.cards.yellow1}</div>
<div class="text-center">Cartons jaunes</div>
<div>${match.stats.cards.yellow2}</div>
<div class="text-right">$
{match.stats.cards.red1}</div>
<div class="text-center">Cartons rouges</div>
<div>${match.stats.cards.red2}</div>
<div class="text-right">$
{match.xg.team1.toFixed(2)}</div>
<div class="text-center">xG</div>
<div>${match.xg.team2.toFixed(2)}</div>
</div>
</div>
<div class="card">
<h4 class="font-bold mb-2">Moments clés</h4>
<ul class="text-sm space-y-1" style="max-height:
200px; overflow-y: auto;">
${match.events.map(event =>
`<li><span class="font-bold">$
{event.minute}':</span> ${event.description}</li>`
).join('')}
</ul>
</div>
</div>
</div>
</div>
`;
goalTeam.textContent = team;
goalAnimation.style.display = 'flex';
if (simulationPaused) {
pauseResumeBtn.innerHTML = '<i class="fas fa-play"></i> Reprendre';
clearTimeout(simulationTimer);
showNotification('Simulation en pause', 'info');
} else {
pauseResumeBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
simulateMatchMinutes();
showNotification('Simulation reprise', 'info');
}
}
function stopSimulation() {
if (!currentMatch) return;
showConfirmation(
'Êtes-vous sûr de vouloir arrêter la simulation ?',
function() {
clearTimeout(simulationTimer);
function newSimulation() {
// Réinitialiser l'interface
document.getElementById('simulationContainer').style.display = 'none';
document.getElementById('matchSummary').style.display = 'none';
document.getElementById('matchSetupCard').style.display = 'block';
function changeSimulationSpeed(speed) {
// Valider la vitesse
if (speed === 'real') {
CURRENT_SIMULATION_SPEED = DEFAULT_SIMULATION_SPEED;
showNotification('Simulation en temps quasi-réel (1 min = 60 sec)',
'info');
} else if (speed === 'fast') {
CURRENT_SIMULATION_SPEED = FAST_SIMULATION_SPEED;
showNotification('Simulation rapide (1 min = 5 sec)', 'info');
} else if (speed === 'very-fast') {
CURRENT_SIMULATION_SPEED = VERY_FAST_SIMULATION_SPEED;
showNotification('Simulation très rapide (1 min = 1 sec)', 'info');
}
}
/* =========================================================
GESTION DES MATCHS PLANIFIÉS
========================================================= */
if (scheduledMatches.length === 0) {
container.innerHTML = '';
emptyIndicator.style.display = 'block';
return;
}
emptyIndicator.style.display = 'none';
html += `
<div class="scheduled-match-item" data-id="${match.id}">
<div>
<div class="scheduled-match-teams">${match.team1} vs $
{match.team2}</div>
<div class="scheduled-match-time">
${status}
${scheduledDate.toLocaleString()} (${timeUntil})
</div>
</div>
<div class="scheduled-match-buttons">
<button class="btn btn-sm btn-primary start-now-btn"
data-id="${match.id}">
<i class="fas fa-play"></i>
</button>
<button class="btn btn-sm btn-danger delete-scheduled-
btn" data-id="${match.id}">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`;
});
container.innerHTML = html;
document.querySelectorAll('.delete-scheduled-btn').forEach(btn => {
btn.addEventListener('click', function() {
const matchId = this.getAttribute('data-id');
deleteScheduledMatch(matchId);
});
});
}
if (diffDays > 0) {
return `Dans ${diffDays} jour${diffDays > 1 ? 's' : ''}`;
} else if (diffHours > 0) {
return `Dans ${diffHours} heure${diffHours > 1 ? 's' : ''}`;
} else if (diffMins > 0) {
return `Dans ${diffMins} minute${diffMins > 1 ? 's' : ''}`;
} else {
return `Dans quelques secondes`;
}
}
showConfirmation(
'Êtes-vous sûr de vouloir supprimer ce match planifié ?',
function() {
// Supprimer le timer associé
if (scheduledMatchesTimers[matchId]) {
clearTimeout(scheduledMatchesTimers[matchId]);
delete scheduledMatchesTimers[matchId];
}
if (!match) {
showNotification('Match planifié non trouvé', 'error');
return;
}
// Démarrer le match
startMatch();
}
// Planifier un match
function scheduleMatchNotification(match) {
const now = Date.now();
const scheduledTime = match.scheduledAt;
setTimeout(() => {
// Envoyer une notification 5 minutes avant
showInAppNotification(
`⏰ Match ${match.team1} vs ${match.team2} dans 5 minutes`,
'info',
true
);
}, preNotificationDelay);
}
/* =========================================================
UTILITAIRES ET FONCTIONS DIVERSES
========================================================= */
// Créer la notification
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.innerHTML = `
<i class="fas ${icon}"></i>
<span>${message}</span>
`;
confirmDialog.innerHTML = `
<div class="bg-card p-6 rounded-lg shadow-lg max-w-sm w-full mx-4"
style="background-color: var(--bg-card); color: var(--text-primary);">
<h3 class="text-lg font-bold mb-4">Confirmation</h3>
<p class="mb-6">${message}</p>
<div class="flex justify-end gap-3">
<button id="confirmCancel" class="btn btn-
secondary">Annuler</button>
<button id="confirmOk" class="btn
btn-danger">Confirmer</button>
</div>
</div>
`;
document.getElementById('confirmOk').addEventListener('click',
function() {
if (typeof onConfirm === 'function') {
onConfirm();
}
confirmDialog.remove();
});
function toggleTheme() {
if (document.body.classList.contains('dark')) {
// Passer au thème clair
document.body.classList.remove('dark');
localStorage.setItem('simulatorTheme', 'light');
document.getElementById('themeToggle').innerHTML = '<i class="fas
fa-moon"></i>';
} else {
// Passer au thème sombre
document.body.classList.add('dark');
localStorage.setItem('simulatorTheme', 'dark');
document.getElementById('themeToggle').innerHTML = '<i class="fas
fa-sun"></i>';
}
}
// Afficher/cacher le loader
function showLoader() {
document.getElementById('loaderContainer').style.display = 'flex';
}
function hideLoader() {
document.getElementById('loaderContainer').style.display = 'none';
}
</script>
</body>
</html>
Pour organiser tous les événements possibles d’un match de football de manière
structurée et exhaustive, on peut les classer en 10 grandes catégories :
---
---
---
---
---
---
---
---
---
---
1️⃣ Attaques
2️⃣ Défenses
3️⃣ Fautes et sanctions
4️⃣ Duels
5️⃣ Gardiens
6️⃣ Coups de pied arrêtés
7️⃣ Dynamique du match
8️⃣ Arbitrage
9️⃣ Conditions extérieures
🔟 Événements exceptionnels
---
nous pouvons aussi ajouter le type de catégorie d'équipe quelle mentionné ci-
dessous
Parfait ! On va maintenant attribuer des probabilités aux événements du match en
fonction des types d’équipes.
---
🔹 Événements Offensifs
CPA dangereux (%) : % d’actions sur coups de pied arrêtés (corners, coups francs).
---
Pressing Haut (%) : Équipe qui récupère le ballon haut sur le terrain.
---
---
3️⃣ Intégration dans la Simulation
D'accord, voici tout le contenu sans tableau pour que tu puisses le recopier
facilement.
---
🔹 Événements Offensifs
---
Top Team : Pressing haut 70 %, bloc médian 20 %, défense basse 10 %, fautes par
match entre 8 et 12, cartons jaunes 15 %, cartons rouges 2 %.
Challenger : Pressing haut 60 %, bloc médian 30 %, défense basse 10 %, fautes par
match entre 10 et 14, cartons jaunes 20 %, cartons rouges 3 %.
---
---
👉 D'accord, voici tout le contenu sans tableau pour que tu puisses le recopier
facilement.
---
🔹 Événements Offensifs
---
Top Team : Pressing haut 70 %, bloc médian 20 %, défense basse 10 %, fautes par
match entre 8 et 12, cartons jaunes 15 %, cartons rouges 2 %.
---
---
donc ajoute aussi tout ça ci-dessous tous les éléments ci-dessous au complet tout
ce que je te demande doit être intégré au complet ne saute rien car ça pourrait
aider pour un encore plus réaliste et vivant
D'accord, voici une description ultra détaillée des événements minute par minute,
prenant en compte toutes les catégories d’équipes et leurs comportements.
---
---
Relance rapide du gardien : Une équipe essaie de repartir vite après un arrêt.
Bloc défensif compact : Une équipe laisse l’adversaire faire le jeu sans presser.
🔹 Événements Offensifs
Tir repoussé par le gardien : Une frappe est arrêtée sans être captée.
Action avortée : L’attaque est stoppée par une interception ou une erreur
technique.
🔹 Événements Défensifs
Fautes tactiques : Une faute est commise pour casser une action.
Carton rouge : Un joueur est expulsé après une faute dangereuse ou un second jaune.
Corner direct dans la surface : Le ballon arrive directement pour une tête ou une
reprise.
Coup franc repoussé par le mur : Le ballon est dévié avant d’atteindre le but.
Changement tactique : Une équipe modifie son approche (plus offensif, plus
défensif).
Bloc équipe plus haut : L’équipe monte d’un cran et joue plus agressivement.
Bloc équipe plus bas : L’équipe recule pour protéger son avantage.
Rush final : L’équipe qui perd pousse dans les dernières minutes.
🔹 Top Team
🔹 Challenger
🔹 Mid-Table
🔹 Underdog
Défend bas et joue beaucoup sur les longs ballons et les CPA.
🔹 Ultra-Défensive
🔹 Physique
🔹 Technique
🔹 Opportuniste
🔹 Chaotique
---
4️⃣ Vers une Simulation Ultra Réaliste
Tu veux qu’on commence à coder une première version dynamique, ou tu veux d’abord
peaufiner certains détails ?
function poissonProbability(lambda, k) {
return (Math.pow(lambda, k) * Math.exp(-lambda)) / factorial(k);
}
function factorial(n) {
return n === 0 ? 1 : n * factorial(n - 1);
}
class UltimateMatchSimulation {
constructor(team1, team2, category1, category2, form1, form2, shots1, shots2,
shotQuality1, shotQuality2, keyPasses1, keyPasses2, counterAttacks1,
counterAttacks2, defStrength1, defStrength2, h2h, odds1, odds2, matchType, weather,
attendance, age1, age2, possession1, possession2, yellowCards1, yellowCards2,
redCards1, redCards2) {
this.team1 = team1;
this.team2 = team2;
this.category1 = category1;
this.category2 = category2;
this.form1 = form1;
this.form2 = form2;
this.h2h = h2h;
this.odds1 = odds1;
this.odds2 = odds2;
this.matchType = matchType;
this.weather = weather;
this.attendance = attendance;
this.age1 = age1;
this.age2 = age2;
this.possession1 = possession1;
this.possession2 = possession2;
this.yellowCards1 = yellowCards1;
this.yellowCards2 = yellowCards2;
this.redCards1 = redCards1;
this.redCards2 = redCards2;
this.score1 = 0;
this.score2 = 0;
}
adjustForContext() {
if (this.form1 > 3) this.xG1 += 0.2;
if (this.form2 > 3) this.xG2 += 0.2;
if (this.h2h > 3) this.xG1 += 0.2;
if (this.h2h < -3) this.xG2 += 0.2;
switch (this.matchType) {
case "Championnat":
this.xG1 += 0.1;
this.xG2 += 0.1;
break;
case "Derby":
this.xG1 += 0.3, this.xG2 += 0.3;
break;
case "Finale de coupe":
this.xG1 -= 0.2, this.xG2 -= 0.2;
break;
case "Qualification Europe":
this.xG1 += 0.2, this.xG2 += 0.2;
break;
case "Relégation":
this.xG1 += 0.3, this.xG2 += 0.3;
break;
}
simulateMatchMonteCarlo(iterations = 10000) {
this.adjustForContext();
let totalGoals1 = 0;
let totalGoals2 = 0;
return {
team1: this.team1,
team2: this.team2,
score1: this.score1,
score2: this.score2
};
}
simulatePoisson(lambda) {
let L = Math.exp(-lambda);
let k = 0;
let p = 1;
do {
k++;
p *= Math.random();
} while (p > L);
return k - 1;
}
}
donc s'il y a des formulaires à ajouter pour prendre comme sa nouvelle amélioration
intègre