Pawe艂 艁aba
commited on
Commit
路
f77b5cc
1
Parent(s):
8345cd8
zmiana w uruchamianiu proagramu
Browse files- public/index.html +1 -0
- public/script.js +41 -10
- train1.py +244 -0
public/index.html
CHANGED
@@ -11,6 +11,7 @@
|
|
11 |
<h1>K贸艂ko i Krzy偶yk</h1>
|
12 |
<div class="board"></div>
|
13 |
<div class="message"></div>
|
|
|
14 |
</div>
|
15 |
|
16 |
|
|
|
11 |
<h1>K贸艂ko i Krzy偶yk</h1>
|
12 |
<div class="board"></div>
|
13 |
<div class="message"></div>
|
14 |
+
<button class="restart" onclick="restart()">Restart</button>
|
15 |
</div>
|
16 |
|
17 |
|
public/script.js
CHANGED
@@ -1,10 +1,18 @@
|
|
1 |
const board = Array(9).fill(0);
|
|
|
|
|
2 |
const winPatterns = [
|
3 |
[0, 1, 2], [3, 4, 5], [6, 7, 8], // poziome
|
4 |
[0, 3, 6], [1, 4, 7], [2, 5, 8], // pionowe
|
5 |
[0, 4, 8], [2, 4, 6] // przek膮tne
|
6 |
];
|
7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
function checkWin(board) {
|
9 |
for (let pattern of winPatterns) {
|
10 |
const [a, b, c] = pattern;
|
@@ -26,7 +34,9 @@ function initializeBoard() {
|
|
26 |
div.addEventListener('click', () => handleMove(i));
|
27 |
boardElement.appendChild(div);
|
28 |
}
|
29 |
-
|
|
|
|
|
30 |
updateBoardDisplay();
|
31 |
}
|
32 |
|
@@ -47,6 +57,21 @@ function updateBoardDisplay() {
|
|
47 |
messageElement.textContent = 'Komputer wygra艂!';
|
48 |
} else if (winner === -1) {
|
49 |
messageElement.textContent = 'Wygra艂e艣!';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
} else if (!board.includes(0)) {
|
51 |
messageElement.textContent = 'Remis!';
|
52 |
} else {
|
@@ -54,15 +79,7 @@ function updateBoardDisplay() {
|
|
54 |
}
|
55 |
}
|
56 |
|
57 |
-
async function
|
58 |
-
if (board[index] !== 0 || checkWin(board)) return;
|
59 |
-
|
60 |
-
// Ruch gracza
|
61 |
-
board[index] = -1;
|
62 |
-
updateBoardDisplay();
|
63 |
-
|
64 |
-
if (checkWin(board)) return;
|
65 |
-
|
66 |
try {
|
67 |
// Wys艂anie stanu planszy do API
|
68 |
const response = await fetch('/move', {
|
@@ -85,6 +102,20 @@ async function handleMove(index) {
|
|
85 |
}
|
86 |
}
|
87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
88 |
// Inicjalizacja gry i pobranie aktualnego stanu
|
89 |
async function initGame() {
|
90 |
try {
|
|
|
1 |
const board = Array(9).fill(0);
|
2 |
+
const step = [];
|
3 |
+
let fitstMove = true;
|
4 |
const winPatterns = [
|
5 |
[0, 1, 2], [3, 4, 5], [6, 7, 8], // poziome
|
6 |
[0, 3, 6], [1, 4, 7], [2, 5, 8], // pionowe
|
7 |
[0, 4, 8], [2, 4, 6] // przek膮tne
|
8 |
];
|
9 |
|
10 |
+
function restart(){
|
11 |
+
board = Array(9).fill(0);
|
12 |
+
step = [];
|
13 |
+
fitstMove = !fitstMove;
|
14 |
+
}
|
15 |
+
|
16 |
function checkWin(board) {
|
17 |
for (let pattern of winPatterns) {
|
18 |
const [a, b, c] = pattern;
|
|
|
34 |
div.addEventListener('click', () => handleMove(i));
|
35 |
boardElement.appendChild(div);
|
36 |
}
|
37 |
+
if(!fitstMove){
|
38 |
+
apiStep();
|
39 |
+
}
|
40 |
updateBoardDisplay();
|
41 |
}
|
42 |
|
|
|
57 |
messageElement.textContent = 'Komputer wygra艂!';
|
58 |
} else if (winner === -1) {
|
59 |
messageElement.textContent = 'Wygra艂e艣!';
|
60 |
+
let newBoard = []
|
61 |
+
for (let i = 0; i < 9 - 1; i++) {
|
62 |
+
let value = 0;
|
63 |
+
if(board[i]!=0)
|
64 |
+
value = board[i]==1?-1:1;
|
65 |
+
newBoard.push(value);
|
66 |
+
fetch('/savegame', {
|
67 |
+
method: 'POST',
|
68 |
+
headers: {
|
69 |
+
'Content-Type': 'application/json'
|
70 |
+
},
|
71 |
+
body: JSON.stringify({ board: newBoard })
|
72 |
+
});
|
73 |
+
}
|
74 |
+
|
75 |
} else if (!board.includes(0)) {
|
76 |
messageElement.textContent = 'Remis!';
|
77 |
} else {
|
|
|
79 |
}
|
80 |
}
|
81 |
|
82 |
+
async function apiStep() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
83 |
try {
|
84 |
// Wys艂anie stanu planszy do API
|
85 |
const response = await fetch('/move', {
|
|
|
102 |
}
|
103 |
}
|
104 |
|
105 |
+
async function handleMove(index) {
|
106 |
+
if (board[index] !== 0 || checkWin(board)) return;
|
107 |
+
|
108 |
+
// Ruch gracza
|
109 |
+
board[index] = -1;
|
110 |
+
updateBoardDisplay();
|
111 |
+
// dodaj gkrok do historii
|
112 |
+
step.push(index);
|
113 |
+
|
114 |
+
if (checkWin(board)) return;
|
115 |
+
|
116 |
+
apiStep();
|
117 |
+
}
|
118 |
+
|
119 |
// Inicjalizacja gry i pobranie aktualnego stanu
|
120 |
async function initGame() {
|
121 |
try {
|
train1.py
ADDED
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
from tensorflow import keras
|
3 |
+
from tensorflow.keras import layers
|
4 |
+
import json
|
5 |
+
import random
|
6 |
+
import argparse
|
7 |
+
import os
|
8 |
+
import hashlib
|
9 |
+
from pathlib import Path
|
10 |
+
|
11 |
+
class TicTacToeTrainer:
|
12 |
+
def __init__(self):
|
13 |
+
self.model = None
|
14 |
+
|
15 |
+
def create_model(self):
|
16 |
+
"""Tworzy model sieci neuronowej"""
|
17 |
+
model = keras.Sequential([
|
18 |
+
layers.Dense(128, activation='relu', input_shape=(9,)),
|
19 |
+
layers.Dropout(0.3),
|
20 |
+
layers.Dense(64, activation='relu'),
|
21 |
+
layers.Dropout(0.3),
|
22 |
+
layers.Dense(9, activation='softmax')
|
23 |
+
])
|
24 |
+
|
25 |
+
model.compile(
|
26 |
+
optimizer='adam',
|
27 |
+
loss='categorical_crossentropy',
|
28 |
+
metrics=['accuracy']
|
29 |
+
)
|
30 |
+
return model
|
31 |
+
|
32 |
+
def calculate_board_hash(self, board):
|
33 |
+
"""Calculate a unique hash for the board state"""
|
34 |
+
return hashlib.md5(str(board.tolist()).encode()).hexdigest()
|
35 |
+
|
36 |
+
def check_two_in_line(self, board, player):
|
37 |
+
"""
|
38 |
+
Check if player has two in a line and return the winning move position
|
39 |
+
Returns: Position to block or None if no blocking needed
|
40 |
+
"""
|
41 |
+
winning_combinations = [
|
42 |
+
[0, 1, 2], [3, 4, 5], [6, 7, 8], # Horizontal
|
43 |
+
[0, 3, 6], [1, 4, 7], [2, 5, 8], # Vertical
|
44 |
+
[0, 4, 8], [2, 4, 6] # Diagonal
|
45 |
+
]
|
46 |
+
|
47 |
+
for combo in winning_combinations:
|
48 |
+
line = board[combo]
|
49 |
+
if sum(line == player) == 2 and sum(line == 0) == 1:
|
50 |
+
# Return the empty position in the line
|
51 |
+
return combo[list(line).index(0)]
|
52 |
+
return None
|
53 |
+
|
54 |
+
def check_winner(self, board):
|
55 |
+
"""
|
56 |
+
Sprawdza czy jest zwyci臋zca lub czy mamy dwa znaki w linii
|
57 |
+
Returns: (bool, str) - (czy wygrana/potencjalna wygrana, typ sytuacji)
|
58 |
+
"""
|
59 |
+
winning_combinations = [
|
60 |
+
[0, 1, 2], [3, 4, 5], [6, 7, 8], # Horizontal
|
61 |
+
[0, 3, 6], [1, 4, 7], [2, 5, 8], # Vertical
|
62 |
+
[0, 4, 8], [2, 4, 6] # Diagonal
|
63 |
+
]
|
64 |
+
|
65 |
+
# Sprawd藕 pe艂n膮 wygran膮 (3 w linii)
|
66 |
+
for combo in winning_combinations:
|
67 |
+
if sum(board[combo]) == 3:
|
68 |
+
return True, "win"
|
69 |
+
|
70 |
+
# Sprawd藕 czy mamy dwa w linii z pustym polem
|
71 |
+
for combo in winning_combinations:
|
72 |
+
line = board[combo]
|
73 |
+
if sum(line == 1) == 2 and sum(line == 0) == 1:
|
74 |
+
return True, "two_in_line"
|
75 |
+
|
76 |
+
return False, "none"
|
77 |
+
|
78 |
+
def generate_training_data(self, num_games=1000):
|
79 |
+
"""
|
80 |
+
Generates unique training data including two-in-line positions
|
81 |
+
"""
|
82 |
+
X = []
|
83 |
+
y = []
|
84 |
+
games_hash_set = set()
|
85 |
+
|
86 |
+
# Load existing games if file exists
|
87 |
+
json_file = Path('games_data.json')
|
88 |
+
if json_file.exists():
|
89 |
+
try:
|
90 |
+
with open(json_file, 'r') as file:
|
91 |
+
existing_games = json.load(file)
|
92 |
+
for game in existing_games:
|
93 |
+
games_hash_set.add(game['hash'])
|
94 |
+
print(f"Loaded {len(existing_games)} existing games")
|
95 |
+
except json.JSONDecodeError:
|
96 |
+
print("Error reading JSON file. Starting with empty games list.")
|
97 |
+
existing_games = []
|
98 |
+
else:
|
99 |
+
existing_games = []
|
100 |
+
|
101 |
+
new_games = []
|
102 |
+
games_generated = 0
|
103 |
+
attempts = 0
|
104 |
+
max_attempts = num_games * 10
|
105 |
+
|
106 |
+
while games_generated < num_games and attempts < max_attempts:
|
107 |
+
attempts += 1
|
108 |
+
board = np.zeros((9,), dtype=int)
|
109 |
+
game_states = []
|
110 |
+
game_moves = []
|
111 |
+
full_sequence = []
|
112 |
+
|
113 |
+
while True:
|
114 |
+
current_state = board.copy()
|
115 |
+
valid_moves = np.where(board == 0)[0]
|
116 |
+
|
117 |
+
if len(valid_moves) == 0:
|
118 |
+
break
|
119 |
+
|
120 |
+
# Player 1 move
|
121 |
+
move = random.choice(valid_moves)
|
122 |
+
move_one_hot = np.zeros(9)
|
123 |
+
move_one_hot[move] = 1
|
124 |
+
|
125 |
+
game_states.append(current_state.copy())
|
126 |
+
game_moves.append(move_one_hot)
|
127 |
+
full_sequence.append({'player': 'X', 'move': int(move)})
|
128 |
+
|
129 |
+
board[move] = 1
|
130 |
+
|
131 |
+
# Sprawd藕 wygran膮 lub dwa w linii
|
132 |
+
is_winning, situation = self.check_winner(board)
|
133 |
+
if is_winning or len(np.where(board == 0)[0]) == 0:
|
134 |
+
break
|
135 |
+
|
136 |
+
# Player 2 move (defensive)
|
137 |
+
valid_moves = np.where(board == 0)[0]
|
138 |
+
if len(valid_moves) > 0:
|
139 |
+
blocking_move = self.check_two_in_line(board, 1)
|
140 |
+
if blocking_move is not None and board[blocking_move] == 0:
|
141 |
+
opponent_move = blocking_move
|
142 |
+
else:
|
143 |
+
opponent_move = random.choice(valid_moves)
|
144 |
+
board[opponent_move] = -1
|
145 |
+
full_sequence.append({'player': 'O', 'move': int(opponent_move)})
|
146 |
+
|
147 |
+
# Calculate hash for the game
|
148 |
+
game_hash = self.calculate_board_hash(board)
|
149 |
+
|
150 |
+
# If game is unique and ended in a win or two-in-line
|
151 |
+
is_winning, situation = self.check_winner(board)
|
152 |
+
if game_hash not in games_hash_set and is_winning:
|
153 |
+
games_hash_set.add(game_hash)
|
154 |
+
games_generated += 1
|
155 |
+
|
156 |
+
game_data = {
|
157 |
+
'hash': game_hash,
|
158 |
+
'moves': full_sequence,
|
159 |
+
'final_board': board.tolist(),
|
160 |
+
'win': situation == "win",
|
161 |
+
'situation': situation
|
162 |
+
}
|
163 |
+
new_games.append(game_data)
|
164 |
+
|
165 |
+
X.extend(game_states)
|
166 |
+
y.extend(game_moves)
|
167 |
+
|
168 |
+
if games_generated % 10 == 0:
|
169 |
+
print(f"Generated {games_generated}/{num_games} unique games")
|
170 |
+
print(f"Last game situation: {situation}")
|
171 |
+
|
172 |
+
all_games = existing_games + new_games
|
173 |
+
|
174 |
+
with open(json_file, 'w') as file:
|
175 |
+
json.dump(all_games, file, indent=2)
|
176 |
+
|
177 |
+
print(f"\nGenerated {len(new_games)} new unique games")
|
178 |
+
print(f"Total games in database: {len(all_games)}")
|
179 |
+
|
180 |
+
return np.array(X), np.array(y)
|
181 |
+
|
182 |
+
|
183 |
+
|
184 |
+
def train(self, epochs=50, games=1000, model_path='model'):
|
185 |
+
"""Trenuje model i zapisuje go do pliku"""
|
186 |
+
print(f"Rozpoczynam generowanie danych treningowych ({games} gier)...")
|
187 |
+
X_train, y_train = self.generate_training_data(games)
|
188 |
+
|
189 |
+
if len(X_train) == 0:
|
190 |
+
print("Nie uda艂o si臋 wygenerowa膰 偶adnych danych treningowych!")
|
191 |
+
return
|
192 |
+
|
193 |
+
print(f"\nWygenerowano dane treningowe: {len(X_train)} przyk艂ad贸w")
|
194 |
+
print(f"Przyk艂adowy stan planszy: {X_train[0]}")
|
195 |
+
|
196 |
+
print(f"\nRozpoczynam trening ({epochs} epok)...")
|
197 |
+
self.model = self.create_model()
|
198 |
+
|
199 |
+
history = self.model.fit(
|
200 |
+
X_train,
|
201 |
+
y_train,
|
202 |
+
epochs=epochs,
|
203 |
+
batch_size=32,
|
204 |
+
validation_split=0.1,
|
205 |
+
verbose=1
|
206 |
+
)
|
207 |
+
|
208 |
+
# Tworzenie katalogu je艣li nie istnieje
|
209 |
+
os.makedirs(model_path, exist_ok=True)
|
210 |
+
|
211 |
+
# Zapisywanie modelu
|
212 |
+
self.model.save(model_path + "/model.keras")
|
213 |
+
print(f"\nModel zosta艂 zapisany w: {model_path}")
|
214 |
+
|
215 |
+
# Zapisywanie metryk treningu
|
216 |
+
metrics = {
|
217 |
+
'accuracy': float(history.history['accuracy'][-1]),
|
218 |
+
'val_accuracy': float(history.history['val_accuracy'][-1]),
|
219 |
+
'loss': float(history.history['loss'][-1]),
|
220 |
+
'val_loss': float(history.history['val_loss'][-1])
|
221 |
+
}
|
222 |
+
|
223 |
+
print("\nWyniki treningu:")
|
224 |
+
print(f"Dok艂adno艣膰: {metrics['accuracy']:.4f}")
|
225 |
+
print(f"Dok艂adno艣膰 walidacji: {metrics['val_accuracy']:.4f}")
|
226 |
+
print(f"Strata: {metrics['loss']:.4f}")
|
227 |
+
print(f"Strata walidacji: {metrics['val_loss']:.4f}")
|
228 |
+
|
229 |
+
def main():
|
230 |
+
parser = argparse.ArgumentParser(description='Trenuj model AI do gry w k贸艂ko i krzy偶yk')
|
231 |
+
parser.add_argument('--epochs', type=int, default=50,
|
232 |
+
help='Liczba epok treningu (domy艣lnie: 50)')
|
233 |
+
parser.add_argument('--games', type=int, default=1000,
|
234 |
+
help='Liczba gier treningowych (domy艣lnie: 1000)')
|
235 |
+
parser.add_argument('--model-path', type=str, default='model',
|
236 |
+
help='艢cie偶ka do zapisania modelu (domy艣lnie: "model")')
|
237 |
+
|
238 |
+
args = parser.parse_args()
|
239 |
+
|
240 |
+
trainer = TicTacToeTrainer()
|
241 |
+
trainer.train(epochs=args.epochs, games=args.games, model_path=args.model_path)
|
242 |
+
|
243 |
+
if __name__ == "__main__":
|
244 |
+
main()
|