Skip to content

Commit 2e986ae

Browse files
Merge pull request #607 from Mudita-Singh/feature/adaptive-rps
feat: add adaptive AI opponent with difficulty levels and interactive UI enhancements
2 parents b83f71f + c5dea06 commit 2e986ae

6 files changed

Lines changed: 721 additions & 604 deletions

File tree

.gitignore

14 Bytes
Binary file not shown.

games/Rock-Paper-Scissor/Rock-Paper-Scissor.py

Lines changed: 119 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,123 @@ def __init__(self):
55
"""Initializes the game state without blocking execution."""
66
self.user_score = 0
77
self.computer_score = 0
8-
self.rounds_played = 0
8+
self.rounds_played = 0
99
# Perfectly mirrors the JS choices array ['rock', 'paper', 'scissors']
1010
self.choices = ["rock", "paper", "scissors"]
11+
# Counter look-up: what beats each move
12+
self.beaten_by = {"rock": "paper", "paper": "scissors", "scissors": "rock"}
13+
# Player history for adaptive AI (capped at recent 20 moves)
14+
self.player_history = []
15+
self.HISTORY_CAP = 20
16+
self.MIN_ADAPTIVE = 3 # rounds before adaptive mode activates
17+
self.ADAPT_RATE = 0.70 # probability of playing counter vs random
1118

19+
# ── Adaptive AI logic ────────────────────────────────────────────────
20+
def _get_move_frequencies(self):
21+
"""Returns overall frequency dict for player history."""
22+
freq = {"rock": 0, "paper": 0, "scissors": 0}
23+
for move in self.player_history:
24+
freq[move] += 1
25+
return freq
26+
27+
def _get_markov_transitions(self, last_move):
28+
"""Returns transition counts from last_move to the next move in history."""
29+
transitions = {"rock": 0, "paper": 0, "scissors": 0}
30+
for i in range(len(self.player_history) - 1):
31+
if self.player_history[i] == last_move:
32+
transitions[self.player_history[i + 1]] += 1
33+
return transitions
34+
35+
def _predict_player_move(self):
36+
"""
37+
Uses a blended Markov-chain + frequency model to predict the player's
38+
next move. Returns (predicted_move, confidence_pct) or (None, None)
39+
if there is not enough data.
40+
"""
41+
n = len(self.player_history)
42+
if n < self.MIN_ADAPTIVE:
43+
return None, None
44+
45+
freq = self._get_move_frequencies()
46+
last = self.player_history[-1]
47+
trans = self._get_markov_transitions(last)
48+
total = sum(trans.values())
49+
50+
if total > 0:
51+
# Blend: 60 % Markov, 40 % frequency
52+
blended = {}
53+
for c in self.choices:
54+
blended[c] = (0.6 * trans[c] / total) + (0.4 * freq[c] / n)
55+
predicted = max(blended, key=blended.get)
56+
confidence = round(blended[predicted] * 100)
57+
else:
58+
# Fallback to pure frequency
59+
predicted = max(freq, key=freq.get)
60+
confidence = round(freq[predicted] / n * 100)
61+
62+
return predicted, confidence
63+
64+
def get_adaptive_computer_choice(self):
65+
"""
66+
Returns (computer_choice, predicted_player_move, confidence_pct, mode).
67+
Plays the counter-move 70 % of the time; random otherwise for balance.
68+
"""
69+
predicted, confidence = self._predict_player_move()
70+
71+
if predicted is None:
72+
return random.choice(self.choices), None, None, "learning"
73+
74+
if random.random() < self.ADAPT_RATE:
75+
computer_choice = self.beaten_by[predicted]
76+
mode = "adaptive"
77+
else:
78+
computer_choice = random.choice(self.choices)
79+
mode = "random"
80+
81+
return computer_choice, predicted, confidence, mode
82+
83+
# ── Stats helpers ────────────────────────────────────────────────────
84+
def _most_frequent_choice(self):
85+
if not self.player_history:
86+
return None
87+
freq = self._get_move_frequencies()
88+
return max(freq, key=freq.get)
89+
90+
# ── Gameplay ─────────────────────────────────────────────────────────
1291
def users_play(self):
13-
"""Handles a single round of interaction."""
92+
"""Handles a single round of interaction with adaptive computer logic."""
1493
user_choice = ""
1594
while user_choice not in self.choices:
1695
user_choice = input("Enter your choice (rock, paper, or scissors): ").lower()
1796
if user_choice not in self.choices:
1897
print("Invalid choice. Please choose rock, paper, or scissors.")
1998

20-
computer_choice = random.choice(self.choices)
21-
print(f"Computer chose: {computer_choice}")
99+
# AI decides BEFORE we record the player's move (prediction is based on prior history)
100+
computer_choice, predicted, confidence, mode = self.get_adaptive_computer_choice()
22101

102+
# Print AI brain info after first MIN_ADAPTIVE rounds
103+
if len(self.player_history) >= self.MIN_ADAPTIVE and predicted is not None:
104+
fav = self._most_frequent_choice()
105+
print(f"\n 🧠 Computer Brain [{mode.upper()}]")
106+
print(f" Your favourite move : {fav}")
107+
print(f" Predicted your move : {predicted} ({confidence}% confidence)")
108+
print(f" Computer chose : {computer_choice}")
109+
else:
110+
remaining = self.MIN_ADAPTIVE - len(self.player_history)
111+
if remaining > 0:
112+
print(f"\n 🧠 Computer Brain [LEARNING] — observing for {remaining} more move(s)...")
113+
print(f" Computer chose: {computer_choice}")
114+
115+
# Record player move after AI has decided
116+
self.player_history.append(user_choice)
117+
if len(self.player_history) > self.HISTORY_CAP:
118+
self.player_history.pop(0)
119+
120+
# Determine round winner
23121
if user_choice == computer_choice:
24122
print("It's a Tie! 🤝")
25123
return "tie"
26-
elif (user_choice == "rock" and computer_choice == "scissors") or \
27-
(user_choice == "paper" and computer_choice == "rock") or \
28-
(user_choice == "scissors" and computer_choice == "paper"):
124+
elif self.beaten_by[computer_choice] == user_choice:
29125
print("You Win this round! 🎉")
30126
return "user"
31127
else:
@@ -35,30 +131,39 @@ def users_play(self):
35131
def statistics(self):
36132
"""Displays performance statistics matching the web dashboard metrics."""
37133
print("\n--- Game Statistics ---")
38-
print(f"Rounds Played: {self.rounds_played}")
39-
print(f"Your Score: {self.user_score}")
40-
print(f"Computer Score: {self.computer_score}")
134+
print(f"Rounds Played : {self.rounds_played}")
135+
print(f"Your Score : {self.user_score}")
136+
print(f"Computer Score : {self.computer_score}")
137+
fav = self._most_frequent_choice()
138+
if fav:
139+
freq = self._get_move_frequencies()
140+
pct = round(freq[fav] / len(self.player_history) * 100)
141+
print(f"Your Favourite : {fav} ({pct}% of plays)")
41142

42143
def save_game(self):
43144
"""Appends the final game results to a local tracking log."""
44145
name = input("Enter your name to save the results (optional): ")
45146
if not name:
46147
name = "Anonymous"
47-
result_string = f"Player: {name}, Final Score: {self.user_score} - {self.computer_score} (User-Computer), Rounds: {self.rounds_played}\n"
148+
result_string = (
149+
f"Player: {name}, Final Score: {self.user_score} - {self.computer_score} "
150+
f"(User-Computer), Rounds: {self.rounds_played}\n"
151+
)
48152
try:
49153
with open("game_results.txt", "a") as f:
50154
f.write(result_string)
51155
print("Game results saved successfully.")
52156
except IOError:
53157
print("Error: Could not save game results to file.")
54158

55-
def play_game(self):
159+
def play_game(self):
56160
"""Launches the primary interactive gameplay loop."""
57161
print("Welcome to Rock, Paper, Scissors!")
162+
print("The computer will learn your patterns and adapt — good luck! 🧠")
58163
while True:
59164
self.rounds_played += 1
60165
print(f"\n--- Round {self.rounds_played} ---")
61-
166+
62167
round_winner = self.users_play()
63168

64169
if round_winner == "user":
@@ -74,6 +179,7 @@ def play_game(self):
74179
self.save_game()
75180
break
76181

182+
77183
# Standard execution block ensuring clean instantiation
78184
if __name__ == "__main__":
79185
game = Rock_Paper_Scissors()

web-app/index.html

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,6 +1097,13 @@
10971097
</div>
10981098
</div>
10991099
</nav>
1100+
<!-- CENTER SEARCH -->
1101+
<div class="search-box-container">
1102+
<div class="search-box">
1103+
<i class="fas fa-search"></i>
1104+
<input aria-label="Search projects" id="heroSearchInput" placeholder="Search projects or games..." type="text"/>
1105+
</div>
1106+
</div>
11001107
<!-- ── Sticky Category Filter Bar ─────────────────────────────── -->
11011108
<!-- Appears below navbar when user scrolls past the hero section -->
11021109
<main id="main-content" tabindex="-1">
@@ -1740,14 +1747,13 @@ <h3>Stay Updated</h3>
17401747
<script src="js/hero-canvas.js"></script>
17411748
<!-- Search Functionality -->
17421749
<script>
1743-
const searchInput = document.getElementById("searchInput");
1750+
const navSearchInput = document.getElementById("searchInput");
1751+
const heroSearchInput = document.getElementById("heroSearchInput");
17441752

17451753
// Select all project cards
17461754
const projectCards = document.querySelectorAll(".project-card");
17471755

1748-
searchInput.addEventListener("input", () => {
1749-
const searchValue = searchInput.value.toLowerCase();
1750-
1756+
function filterCards(searchValue) {
17511757
projectCards.forEach(card => {
17521758
const title = card.querySelector("h3").textContent.toLowerCase();
17531759
const description = card.querySelector("p").textContent.toLowerCase();
@@ -1761,7 +1767,23 @@ <h3>Stay Updated</h3>
17611767
card.style.display = "none";
17621768
}
17631769
});
1764-
});
1770+
}
1771+
1772+
// Sync both search inputs and filter cards
1773+
if (navSearchInput) {
1774+
navSearchInput.addEventListener("input", () => {
1775+
const val = navSearchInput.value.toLowerCase();
1776+
if (heroSearchInput) heroSearchInput.value = navSearchInput.value;
1777+
filterCards(val);
1778+
});
1779+
}
1780+
if (heroSearchInput) {
1781+
heroSearchInput.addEventListener("input", () => {
1782+
const val = heroSearchInput.value.toLowerCase();
1783+
if (navSearchInput) navSearchInput.value = heroSearchInput.value;
1784+
filterCards(val);
1785+
});
1786+
}
17651787

17661788
// Handle Scroll Transitions to resize the floating layout dynamically
17671789
const mainNavbar = document.getElementById("mainNavbar");

0 commit comments

Comments
 (0)