Skip to content

Commit 061c609

Browse files
Merge pull request #2 from CodeKunalTomar/copilot/remove-difficulty-system
Transform OptiConnect: Elite AI with Chess-Style Timers
2 parents a833cff + 48312cc commit 061c609

File tree

6 files changed

+383
-154
lines changed

6 files changed

+383
-154
lines changed

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Test files
2+
test-*.html
3+
test-*.js
4+
5+
# macOS
6+
.DS_Store
7+
8+
# Editor files
9+
*.swp
10+
*.swo
11+
*~
12+
.vscode/
13+
.idea/
14+
15+
# Logs
16+
*.log

Connect-4.css

Lines changed: 48 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -55,76 +55,82 @@ h2 {
5555
border-radius: 8px;
5656
}
5757

58-
.dif-options {
59-
clear: both;
60-
overflow: hidden;
61-
margin: 20px -7px 0;
58+
/* Timer Panel */
59+
.timer-panel {
60+
display: flex;
61+
flex-direction: column;
62+
gap: 10px;
63+
padding: 15px;
6264
}
6365

64-
.dif-options div {
65-
float: left;
66-
width: 20%;
66+
.timer {
67+
padding: 15px;
68+
background-color: rgba(255, 255, 255, 0.1);
69+
border-radius: 8px;
70+
font-family: "Doppio One", monospace;
71+
text-align: center;
72+
border: 2px solid transparent;
73+
transition: all 0.3s ease;
6774
}
6875

69-
.dif-options input {
70-
display: none;
76+
.timer.active {
77+
border-color: #4CAF50;
78+
box-shadow: 0 0 15px rgba(76, 175, 80, 0.5);
79+
background-color: rgba(76, 175, 80, 0.2);
7180
}
7281

73-
.dif-options input:checked+label {
74-
color: #fff;
75-
background-color: #593f6b;
76-
border-color: #fff;
77-
cursor: default;
82+
.timer.warning {
83+
border-color: #f44336;
84+
background-color: rgba(244, 67, 54, 0.2);
85+
animation: pulse 1s infinite;
7886
}
7987

80-
.dif-options label {
81-
display: block;
82-
margin: 0 auto;
83-
width: 24px;
84-
height: 24px;
85-
background-color: #666;
86-
border: solid 2px #ccc;
87-
border-radius: 8px;
88-
color: #999;
89-
text-align: center;
90-
line-height: 24px;
91-
cursor: pointer;
88+
@keyframes pulse {
89+
0%, 100% { opacity: 1; }
90+
50% { opacity: 0.6; }
9291
}
9392

94-
.freeze .dif-options input:not(:checked)+label {
95-
font-size: 0;
96-
margin: 7px auto;
97-
width: 10px;
98-
height: 10px;
99-
border-radius: 4px;
100-
color: transparent;
101-
line-height: 10px;
102-
cursor: default;
103-
transition: .2s
93+
.timer-label {
94+
font-size: 14px;
95+
color: #aaa;
96+
margin-bottom: 5px;
10497
}
10598

106-
.start {
107-
margin-top: 20px;
99+
.timer-value {
100+
font-size: 32px;
101+
color: #fff;
102+
font-weight: bold;
108103
}
109104

110-
.start button {
105+
/* Start Panel */
106+
.start-panel {
107+
padding: 12px;
108+
}
109+
110+
.start-button {
111111
display: block;
112112
width: 100%;
113-
padding: 2px 12px 4px;
113+
padding: 8px 12px;
114114
font-family: inherit;
115115
font-size: 24px;
116116
border: solid 2px #ccc;
117117
border-radius: 8px;
118118
background-color: #593f6b;
119119
color: #fff;
120120
cursor: pointer;
121+
transition: all 0.2s;
122+
}
123+
124+
.start-button:hover {
125+
background-color: #6d4d82;
126+
transform: scale(1.02);
121127
}
122128

123-
.start button:focus {
129+
.start-button:focus {
124130
outline: none;
125131
}
126132

127-
.freeze .start {
133+
.start-panel.freeze .start-button {
128134
display: none;
129135
}
130136

Connect-4.js

Lines changed: 103 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ const MAX_TT_SIZE = 1000000; // Max entries in transposition table
1515
const BOARD_HEIGHT = TOTAL_ROWS + 1; // Extra row for overflow detection
1616
const BOARD_WIDTH = TOTAL_COLUMNS;
1717

18+
// Opening book - prioritize center column
19+
const OPENING_BOOK = {
20+
'': 3, // First move - always play center column
21+
};
22+
const MAX_OPENING_MOVES = 2; // Only use opening book for first 2 moves
23+
24+
// Column ordering for move ordering (center columns first for better alpha-beta pruning)
25+
const COLUMN_ORDER = [3, 2, 4, 1, 5, 0, 6];
26+
27+
// Position evaluation weights
28+
const CENTER_COLUMN_WEIGHT = 3;
29+
const CENTER_ADJACENT_WEIGHT = 2;
30+
1831
// Initialize Zobrist hashing table (random 64-bit values for each position and player)
1932
const zobristTable = [];
2033
function initZobrist() {
@@ -239,6 +252,53 @@ GameState.prototype.getPlayerForChipAt = function(col, row) {
239252
return player;
240253
}
241254

255+
// Evaluate position heuristically for non-terminal positions
256+
GameState.prototype.evaluatePosition = function(player) {
257+
let score = 0;
258+
259+
// Center control - pieces in center columns are more valuable
260+
for (let row = 0; row < this.bitboard.heights[3]; row++) {
261+
if (this.board[3][row] === player) {
262+
score += CENTER_COLUMN_WEIGHT;
263+
} else if (this.board[3][row] !== undefined) {
264+
score -= CENTER_COLUMN_WEIGHT;
265+
}
266+
}
267+
268+
// Adjacent to center also valuable
269+
for (let col of [2, 4]) {
270+
for (let row = 0; row < this.bitboard.heights[col]; row++) {
271+
if (this.board[col][row] === player) {
272+
score += CENTER_ADJACENT_WEIGHT;
273+
} else if (this.board[col][row] !== undefined) {
274+
score -= CENTER_ADJACENT_WEIGHT;
275+
}
276+
}
277+
}
278+
279+
// Normalize score to be within minimax range
280+
return score * 0.1;
281+
}
282+
283+
// Get a simple board state hash for opening book lookup
284+
function getBoardStateKey(gameState) {
285+
let key = '';
286+
let moveCount = 0;
287+
for (let col = 0; col < TOTAL_COLUMNS; col++) {
288+
moveCount += gameState.board[col].length;
289+
}
290+
291+
// Only use opening book for first few moves
292+
if (moveCount > MAX_OPENING_MOVES) return null;
293+
294+
for (let col = 0; col < TOTAL_COLUMNS; col++) {
295+
for (let row = 0; row < gameState.board[col].length; row++) {
296+
key += col + '' + gameState.board[col][row];
297+
}
298+
}
299+
return key;
300+
}
301+
242302
// listen for messages from the main thread
243303
self.addEventListener('message', function(e) {
244304
switch(e.data.messageType) {
@@ -285,27 +345,40 @@ function makeComputerMove(maxDepth) {
285345
let isWinImminent = false;
286346
let isLossImminent = false;
287347

288-
for (let depth = 0; depth <= maxDepth; depth++) {
289-
const origin = new GameState(currentGameState);
290-
const isTopLevel = (depth === maxDepth);
291-
292-
// Alpha-beta search with initial bounds
293-
const tentativeCol = think(origin, 2, depth, isTopLevel, -Infinity, Infinity);
294-
295-
if (origin.score === HUMAN_WIN_SCORE) {
296-
// AI realizes it can lose, thinks all moves suck now, keep move picked at previous depth
297-
// this solves the "apathy" problem
298-
isLossImminent = true;
299-
break;
300-
} else if (origin.score === COMPUTER_WIN_SCORE) {
301-
// AI knows how to win, no need to think deeper, use this move
302-
// this solves the "cocky" problem
303-
col = tentativeCol;
304-
isWinImminent = true;
305-
break;
306-
} else {
307-
// go with this move, for now at least
308-
col = tentativeCol;
348+
// Check opening book first
349+
const boardKey = getBoardStateKey(currentGameState);
350+
if (boardKey !== null && boardKey in OPENING_BOOK) {
351+
const openingCol = OPENING_BOOK[boardKey];
352+
// Verify move is valid
353+
if (currentGameState.bitboard.heights[openingCol] < TOTAL_ROWS) {
354+
col = openingCol;
355+
}
356+
}
357+
358+
if (col === undefined) {
359+
// Use iterative deepening with fixed high depth
360+
for (let depth = 0; depth <= maxDepth; depth++) {
361+
const origin = new GameState(currentGameState);
362+
const isTopLevel = (depth === maxDepth);
363+
364+
// Alpha-beta search with initial bounds
365+
const tentativeCol = think(origin, 2, depth, isTopLevel, -Infinity, Infinity);
366+
367+
if (origin.score === HUMAN_WIN_SCORE) {
368+
// AI realizes it can lose, thinks all moves suck now, keep move picked at previous depth
369+
// this solves the "apathy" problem
370+
isLossImminent = true;
371+
break;
372+
} else if (origin.score === COMPUTER_WIN_SCORE) {
373+
// AI knows how to win, no need to think deeper, use this move
374+
// this solves the "cocky" problem
375+
col = tentativeCol;
376+
isWinImminent = true;
377+
break;
378+
} else {
379+
// go with this move, for now at least
380+
col = tentativeCol;
381+
}
309382
}
310383
}
311384

@@ -350,13 +423,14 @@ function think(node, player, recursionsRemaining, isTopLevel, alpha, beta) {
350423
}
351424
}
352425

353-
let col;
354426
let scoreSet = false;
355427
const childNodes = [];
356428
let bestMove = -1;
357429

358-
// consider each column as a potential move
359-
for (col = 0; col < TOTAL_COLUMNS; col++) {
430+
// Use column ordering for better alpha-beta pruning (center columns first)
431+
for (let colIdx = 0; colIdx < COLUMN_ORDER.length; colIdx++) {
432+
const col = COLUMN_ORDER[colIdx];
433+
360434
if(isTopLevel) {
361435
self.postMessage({
362436
messageType: 'progress',
@@ -376,6 +450,10 @@ function think(node, player, recursionsRemaining, isTopLevel, alpha, beta) {
376450
// no game stopping win and there are still recursions to make, think deeper
377451
const nextPlayer = (player === 1) ? 2 : 1;
378452
think(childNode, nextPlayer, recursionsRemaining - 1, false, alpha, beta);
453+
} else if (!childNode.isWin() && recursionsRemaining === 0) {
454+
// At leaf node, apply heuristic evaluation
455+
const heuristicScore = childNode.evaluatePosition(2); // Evaluate for computer
456+
childNode.score = heuristicScore;
379457
}
380458

381459
if (!scoreSet) {
@@ -433,7 +511,7 @@ function think(node, player, recursionsRemaining, isTopLevel, alpha, beta) {
433511
// For non-top level, just return the best move (may have been pruned)
434512
if (isTopLevel) {
435513
const candidates = [];
436-
for (col = 0; col < TOTAL_COLUMNS; col++) {
514+
for (let col = 0; col < TOTAL_COLUMNS; col++) {
437515
if (childNodes[col] !== undefined && childNodes[col].score === node.score) {
438516
candidates.push(col);
439517
}

0 commit comments

Comments
 (0)