first commit

This commit is contained in:
Carl Furtado
2026-08-19 14:03:22 -04:00
commit dd50c850ac
16 changed files with 6389 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
*.png
*.txt
Todo.md
data-dir/
__pycache__/
+4
View File
@@ -0,0 +1,4 @@
{
"python-envs.defaultEnvManager": "ms-python.python:poetry",
"python-envs.defaultPackageManager": "ms-python.python:poetry",
}
Generated
+5047
View File
File diff suppressed because one or more lines are too long
+25
View File
@@ -0,0 +1,25 @@
[project]
name = "bing-rewards-bot"
version = "0.1.0"
description = "Script to farm MS Rewards points on desktop"
authors = [
{name = "Carl Furtado",email = "carlzfurtado@gmail.com"}
]
license = "MIT"
requires-python = ">=3.14"
dependencies = [
"selenium (>=4.46.0,<5.0.0)",
"matplotlib (>=3.11.1,<4.0.0)",
"pygetwindow (>=0.0.9,<0.0.10)",
"keyboard (>=0.13.5,<0.14.0)",
"pygame-ce (>=2.5.8,<3.0.0)",
"ollama (>=0.6.2,<0.7.0)"
]
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
package-mode = false
+13
View File
@@ -0,0 +1,13 @@
keypress_times: list[float] = [
float(keypress_time) for keypress_time in open("keypress_times.txt").read().splitlines()
]
for i in range(10):
interval_start = i*0.1
interval_end = (i+1)*0.1
within_interval = sum(interval_start <= keypress_time < interval_end for keypress_time in keypress_times)
percent_within_interval = within_interval / len(keypress_times) * 100
print(f"Interval {interval_start:.1f}-{interval_end:.1f}: {within_interval} keypresses ({percent_within_interval:.2f}%)")
+4
View File
@@ -0,0 +1,4 @@
from os.path import abspath
USER_DATA_DIR = abspath("./data-dir")
PROFILE_NAME = "Profile 1"
+138
View File
@@ -0,0 +1,138 @@
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement
from selenium.common.exceptions import NoSuchElementException
from selenium import webdriver
class ElementSelectionUtils:
def __init__(self, driver: webdriver.Edge):
self.driver = driver
def resolve(self, xpath: str):
return self.driver.find_element(By.XPATH, xpath)
def get_earn_tab(self):
return self.resolve('//*[@id="react-aria-_R_18mbslbH1_-tab-/earn"]')
def get_dashboard_tab(self):
return self.resolve('//*[@id="react-aria-_R_18mbslbH1_-tab-/dashboard"]')
def get_open_daily_set_button(self):
return self.resolve("/html/body/div[2]/div[2]/div/main/section[1]/div/div[2]/div/div/button[3]")
def get_open_visual_search_sidebar(self):
return self.resolve("/html/body/div[2]/div[2]/div/main/section[1]/div/div[2]/div/div/button[5]")
def get_sidebar_section(self):
sections = self.driver.find_elements(By.TAG_NAME, "section")
for section in sections:
if section.get_dom_attribute("id").startswith("react-aria"):
return section
raise Exception("Sidebar section not found")
def get_daily_set_elements(self):
daily_set_sidebar = self.get_sidebar_section()
daily_set_elems = daily_set_sidebar.find_elements(By.TAG_NAME, "a")[1:]
return daily_set_elems
def get_explore_on_bing_elements(self):
return [
self.resolve("/html/body/div[2]/div[2]/div/main/section[2]/div/div[2]/div/div/a[1]"),
self.resolve("/html/body/div[2]/div[2]/div/main/section[2]/div/div[2]/div/div/a[2]"),
self.resolve("/html/body/div[2]/div[2]/div/main/section[2]/div/div[2]/div/div/a[3]"),
self.resolve("/html/body/div[2]/div[2]/div/main/section[2]/div/div[2]/div/div/a[4]")
]
def get_search_now_link_from_visual_search_sidebar(self):
visual_search_sidebar = self.get_sidebar_section()
return visual_search_sidebar.find_elements(By.TAG_NAME, "a")[1]
def extract_card_descriptions(self, card: WebElement):
return card.find_element(By.CSS_SELECTOR, "p:nth-child(2)").text
def card_is_complete(self, card: WebElement):
return "completed" in card.find_element(By.CSS_SELECTOR, "div.flex.w-full.items-center.gap-2").text.lower()
def get_bing_search_bar(self):
return self.driver.find_element(By.TAG_NAME, "textarea")
def get_visual_search_button(self):
return self.driver.find_element(By.CSS_SELECTOR, "#sb_form > div.camera.icon")
def get_visual_search_file_input(self):
return self.driver.find_element(By.CSS_SELECTOR, "#sb_fileinput")
def get_all_misc_cards(self):
misc_cards_container = self.driver.find_element(By.ID, "moreactivities")
return misc_cards_container.find_elements(By.TAG_NAME, "a")
def get_card_point_value(self, card: WebElement):
# querySelector("div.flex.w-full.items-center.gap-2").querySelector('p')
try: elem = card.find_element(By.CSS_SELECTOR, "div.flex.w-full.items-center.gap-2").find_element(By.TAG_NAME, "p")
except NoSuchElementException:
return 0
return int(elem.text)
def element_is_fully_in_viewport(self, elem: WebElement) -> bool:
js_viewport_check = """
var elem = arguments[0];
var box = elem.getBoundingClientRect();
// Check if the element is at least partially in the viewport
return (
box.top >= 0 &&
box.left >= 0 &&
box.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
box.right <= (window.innerWidth || document.documentElement.clientWidth)
);
"""
return self.driver.execute_script(js_viewport_check, elem)
def get_points_breakdown_button(self):
elem = self.driver.find_element(By.XPATH, "/html/body/div[2]/div[2]/div/main/div/button[1]")
if "points breakdown" not in elem.text.lower():
raise Exception("Points Breakdown button not found")
return elem
def get_close_button_on_points_breakdown(self):
breakdown_sidebar = self.get_sidebar_section()
return breakdown_sidebar.find_elements(By.TAG_NAME, "button")[2]
def get_points_earned_from_searches_on_points_breakdown(self) -> int:
breakdown_sidebar = self.get_sidebar_section()
fraction = breakdown_sidebar.find_element(By.CSS_SELECTOR, "div.py-3.wrap-anywhere.justify-self-end").text
earned_str, max_str = fraction.split('/')
return int(earned_str.strip()), int(max_str.strip())
def get_bonus_button_on_dashboard(self):
button = self.driver.find_element(By.XPATH, "/html/body/div[2]/div[2]/div/main/div/button[2]")
if "ready to claim" not in button.text.lower():
raise Exception("Bonus button not found")
return button
def get_claim_bonus_points_button(self):
bonus_sidebar = self.get_sidebar_section()
return bonus_sidebar.find_elements(By.TAG_NAME, "button")[2]
def get_generic_sidebar_close_button(self):
sidebar = self.get_sidebar_section()
return sidebar.find_elements(By.TAG_NAME, "button")[0]
+254
View File
@@ -0,0 +1,254 @@
"""Measure Fitts-law constants a and b from repeated target-click trials.
The user starts at a fixed home point, moves to a randomly generated 2D
rectangular target, and clicks it. The movement time begins when the cursor
moves more than a few pixels from the home position and ends when the click is
received. For each trial, the target width term is the average of the target's
height and width: W = (H + W) / 2.
The regression is performed as:
MT = a + b * ID
where:
ID = log2(2D / W)
with D as the distance from the home point to the target center.
"""
from __future__ import annotations
import ctypes
import math
import random
import time
from dataclasses import dataclass
from typing import List, Sequence, Tuple
import tkinter as tk
@dataclass
class Trial:
distance: float
target_width: float
target_height: float
width_term: float
index_of_difficulty: float
movement_time: float
def get_screen_size() -> Tuple[int, int]:
user32 = ctypes.windll.user32
width = user32.GetSystemMetrics(0)
height = user32.GetSystemMetrics(1)
return width, height
def set_cursor_position(x: int, y: int) -> None:
user32 = ctypes.windll.user32
user32.SetCursorPos(int(x), int(y))
def point_in_rectangle(px: float, py: float, x: float, y: float, w: float, h: float) -> bool:
return x <= px <= x + w and y <= py <= y + h
def generate_target(start: Tuple[int, int], screen_w: int, screen_h: int) -> Tuple[float, float, float, float, float]:
margin = 80
for _ in range(1000):
target_w = random.randint(30, 180)
target_h = random.randint(30, 180)
x = random.randint(margin, max(margin, screen_w - target_w - margin))
y = random.randint(margin, max(margin, screen_h - target_h - margin))
center_x = x + target_w / 2
center_y = y + target_h / 2
distance = math.hypot(center_x - start[0], center_y - start[1])
if distance < 100:
continue
return x, y, target_w, target_h, distance
# Fallback if the random search fails.
target_w = 120
target_h = 80
x = screen_w * 0.75
y = screen_h * 0.35
return x, y, target_w, target_h, math.hypot(x + target_w / 2 - start[0], y + target_h / 2 - start[1])
def run_single_trial(start: Tuple[int, int], target_x: float, target_y: float, target_w: float, target_h: float, distance: float, trial_no: int, total_trials: int) -> float:
screen_w, screen_h = get_screen_size()
root = tk.Tk()
root.title("Fitts Law Calibration")
root.attributes("-fullscreen", True)
root.attributes("-topmost", True)
root.configure(bg="#f3f3f3")
canvas = tk.Canvas(root, width=screen_w, height=screen_h, bg="#f3f3f3", highlightthickness=0)
canvas.pack(fill="both", expand=True)
start_x, start_y = start
start_marker = canvas.create_oval(start_x - 14, start_y - 14, start_x + 14, start_y + 14, fill="#1f1f1f")
target_id = canvas.create_rectangle(
target_x,
target_y,
target_x + target_w,
target_y + target_h,
fill="#5c8dff",
outline="#0d2d73",
width=3,
)
canvas.create_text(
screen_w // 2,
48,
text=f"Trial {trial_no}/{total_trials}: move from the center to the blue rectangle and click it.",
font=("Segoe UI", 18),
fill="#111111",
)
set_cursor_position(start_x, start_y)
root.update_idletasks()
root.update()
trial_result = {"movement_time": None}
movement_started = {"value": False}
movement_start_time = {"value": 0.0}
def on_motion(event):
if not movement_started["value"]:
dx = abs(event.x_root - start_x)
dy = abs(event.y_root - start_y)
if max(dx, dy) > 3:
movement_started["value"] = True
movement_start_time["value"] = time.perf_counter()
def on_click(event):
if not movement_started["value"]:
return
if point_in_rectangle(event.x_root, event.y_root, target_x, target_y, target_w, target_h):
trial_result["movement_time"] = time.perf_counter() - movement_start_time["value"]
root.quit()
root.destroy()
return
canvas.create_text(
screen_w // 2,
90,
text="Missed the target. Click the blue rectangle only.",
fill="#d32f2f",
font=("Segoe UI", 16),
)
canvas.update()
root.bind("<Motion>", on_motion)
root.bind("<ButtonPress-1>", on_click)
root.bind("<Escape>", lambda _: (root.destroy(), raise_system_exit()))
root.mainloop()
if trial_result["movement_time"] is None:
raise RuntimeError("Trial ended without a valid target click.")
return trial_result["movement_time"]
def raise_system_exit():
raise SystemExit
def fit_fitts_law(trials: Sequence[Trial]) -> Tuple[float, float, float]:
if not trials:
raise ValueError("At least one trial is required.")
x_values = [trial.index_of_difficulty for trial in trials]
y_values = [trial.movement_time for trial in trials]
x_mean = sum(x_values) / len(x_values)
y_mean = sum(y_values) / len(y_values)
numerator = sum((x - x_mean) * (y - y_mean) for x, y in zip(x_values, y_values))
denominator = sum((x - x_mean) ** 2 for x in x_values)
if denominator == 0:
raise ValueError("Index of difficulty did not vary across trials.")
b = numerator / denominator
a = y_mean - b * x_mean
rss = sum((y - (a + b * x)) ** 2 for x, y in zip(x_values, y_values))
tss = sum((y - y_mean) ** 2 for y in y_values)
r_squared = 1.0 if tss == 0 else 1.0 - (rss / tss)
return a, b, r_squared
def collect_trials(trial_count: int = 15) -> List[Trial]:
screen_w, screen_h = get_screen_size()
start = (screen_w // 2, screen_h // 2)
trials: List[Trial] = []
for trial_no in range(1, trial_count + 1):
target_x, target_y, target_w, target_h, distance = generate_target(start, screen_w, screen_h)
movement_time = run_single_trial(start, target_x, target_y, target_w, target_h, distance, trial_no, trial_count)
width_term = (target_w + target_h) / 2.0
if width_term <= 0:
raise ValueError("Target width must be greater than zero.")
index_of_difficulty = math.log2((2.0 * distance) / width_term)
trials.append(
Trial(
distance=distance,
target_width=target_w,
target_height=target_h,
width_term=width_term,
index_of_difficulty=index_of_difficulty,
movement_time=movement_time,
)
)
return trials
def main() -> None:
try:
trials = collect_trials(trial_count=18)
a, b, r_squared = fit_fitts_law(trials)
print("Fitts Law calibration results")
print("=" * 40)
print(f"Target width term W = (height + width) / 2")
print(f"Regression: MT = {a:.4f} + {b:.4f} * ID")
print(f"R^2 = {r_squared:.4f}")
print("\nSample trials:")
for trial in trials:
print(
f" D={trial.distance:.1f}px, W={trial.width_term:.1f}px, "
f"ID={trial.index_of_difficulty:.3f}, MT={trial.movement_time:.3f}s"
)
result_window = tk.Tk()
result_window.title("Fitts Law Estimate")
result_window.geometry("500x180")
result_window.configure(bg="#ffffff")
label = tk.Label(
result_window,
text=(
f"Estimated model:\nMT = {a:.4f} + {b:.4f} * ID\n"
f"R^2 = {r_squared:.4f}\n\n"
),
font=("Segoe UI", 14),
bg="#ffffff",
justify="left",
padx=20,
pady=20,
)
label.pack(fill="both", expand=True)
result_window.mainloop()
except SystemExit:
pass
except Exception as exc: # pragma: no cover - message shown in console for user feedback.
print(f"An error occurred: {exc}")
raise
if __name__ == "__main__":
main()
+93
View File
@@ -0,0 +1,93 @@
from typing import Generator
import random
import ollama
DEFAULT_SYSTEM_PROMPT_FOR_SEARCH_QUEST = (
"You are a helpful assistant tasked with creating a search query based on a directive. "
"Output nothing but the search query you create, and do not include any additional commentary or explanation. "
"Do not include any labels or quotes. "
"The search query must be the only output, and do not format the query as an imperative to 'search for' something. "
"Imagine that your output will be fed directly into a search engine as you provide it. "
"For example, if the directive is 'Search on Bing for the latest news about space exploration', you might output 'latest news space exploration'. "
"Outputting 'search on Bing for the latest news about space exploration' or 'search bing.com/news for space exploration' would be incorrect, "
"as those answers include instructions to perform a search rather than just the search query itself. "
"Additionally, try to be specific, e.g. if a prompt asks you to search for vacation flights or cruises, include "
"a location where you might want to go on vacation, or a specific cruise line or destination. The current year is 2026."
)
DEFAULT_USER_PROMPT_FOR_SEARCH_QUEST_WITHOUT_DESC = """Base your search query on the following task description: """
DEFAULT_SYSTEM_PROMPT_FOR_SEARCH_POINTS = (
"The user is interested in learning more about topics related to a word that will be given to you. "
"Your task is to come up with subsequent search queries that relate to each other, each one branching out "
"from the previous one so that the user can explore a topic in depth. Your first search query should be "
"based on the word that the user gives you, and each subsequent search query should be at least remotely based on the previous ones. "
"Output only the single search query you come up with and do not include any additional commentary or explanation. Do not include any labels or quotes. "
"The search queries should ideally be short (6 words max) and do not need to be fully fledged questions, but they should be unique. The current year is 2026."
)
DEFAULT_USER_PROMPT_FOR_SEARCH_POINTS_WITHOUT_DESC = """Generate the first search query based on the following word: """
USER_PROMPT_FOR_SEARCH_QUERY_CONTINUATION = """Generate the next search query."""
def get_ollama_response(messages: list[dict[str, str]], model: str="gemma4:cloud") -> str:
response = ollama.chat(
model=model,
messages=messages
)
return response.message.content
def get_search_query_from_task_description(task_description: str) -> str:
# compat
if "lyrics of your favorite song" in task_description.lower(): return "sweet caroline lyrics"
messages = [
{
"role": "system",
"content": DEFAULT_SYSTEM_PROMPT_FOR_SEARCH_QUEST
},
{
"role": "user",
"content": DEFAULT_USER_PROMPT_FOR_SEARCH_QUEST_WITHOUT_DESC + task_description
}
]
while not (response := get_ollama_response(messages)): pass # ensure non-empty response
return response.lower()
def get_related_search_queries(seed_word: str, num_queries: int=20) -> Generator[str, None, None]:
messages = [
{
"role": "system",
"content": DEFAULT_SYSTEM_PROMPT_FOR_SEARCH_POINTS
},
{
"role": "user",
"content": DEFAULT_USER_PROMPT_FOR_SEARCH_POINTS_WITHOUT_DESC + seed_word
}
]
for _ in range(num_queries):
while not (response := get_ollama_response(messages)): pass # ensure non-empty response
yield response.lower()
messages.append({
"role": "assistant",
"content": response
})
messages.append({
"role": "user",
"content": USER_PROMPT_FOR_SEARCH_QUERY_CONTINUATION
})
NOUNS = [
noun.strip().lower() for noun in open("nouns.txt", "r").read().splitlines()
if len(noun.strip()) >= 3
]
def get_random_noun() -> str:
return random.choice(NOUNS)
+28
View File
@@ -0,0 +1,28 @@
import rewards_tasks
import mouse_trajectory
import mimic_typing
from selenium import webdriver
from constants import USER_DATA_DIR, PROFILE_NAME
options = webdriver.EdgeOptions()
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option('useAutomationExtension', False)
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument(f"--user-data-dir={USER_DATA_DIR}")
options.add_argument(f"--profile-directory={PROFILE_NAME}")
driver = webdriver.Edge(options=options)
mouse = mouse_trajectory.MouseUtils(driver)
keyboard = mimic_typing.KeyboardUtils(driver)
driver.get("https://rewards.bing.com/")
rewards = rewards_tasks.RewardsTaskUtils(driver)
rewards.complete_all_tasks()
input("Press Enter to exit...")
driver.quit()
+31
View File
@@ -0,0 +1,31 @@
import random
from typing import Iterable
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
FIRST_INTERVAL = (0.0, 0.1)
SECOND_INTERVAL = (0.1, 0.2)
THIRD_INTERVAL = (0.2, 0.4)
FIRST_INTERVAL_PROBABILITY = 0.377
SECOND_INTERVAL_PROBABILITY = 0.5492
THIRD_INTERVAL_PROBABILITY = 1 - (FIRST_INTERVAL_PROBABILITY + SECOND_INTERVAL_PROBABILITY)
class KeyboardUtils:
def __init__(self, driver: webdriver.Edge):
self.driver = driver
def send_keys(self, keys: Iterable[str]):
actions = ActionChains(self.driver, duration=0)
for key in keys:
actions.send_keys(key)
interval = random.choices(
[FIRST_INTERVAL, SECOND_INTERVAL, THIRD_INTERVAL],
weights=[FIRST_INTERVAL_PROBABILITY, SECOND_INTERVAL_PROBABILITY, THIRD_INTERVAL_PROBABILITY]
)[0]
actions.pause(random.uniform(interval[0], interval[1]))
actions.perform()
+315
View File
@@ -0,0 +1,315 @@
import time
from selenium.webdriver.common.actions.action_builder import ActionBuilder
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.remote.webelement import WebElement
from selenium import webdriver
from functools import partial
import math
import random
import numpy as np
from typing import Callable
Point = tuple[int, int]
DEFAULT_INTERMEDIATE_RADIUS_INTERVAL = (20, 40)
DEFAULT_DEVIATION_INTERVAL = (1, 5)
DEFAULT_DISTORTION_ZONE_TIME_LENGTH = 0.05
DEFAULT_DISTORTION_FREQUENCY = 0.15
def cubic_bezier_single_coordinate(p0: int, p1: int, p2: int, p3: int, t: float):
first_coeff = (1-t)**3
second_coeff = 3*t*(1-t)**2
third_coeff = 3*(1-t)*(t**2)
fourth_coeff = t**3
return (
first_coeff*p0 +
second_coeff*p1 +
third_coeff*p2 +
fourth_coeff*p3
)
def cubic_bezier(p0: Point, p1: Point, p2: Point, p3: Point, t: float) -> Point:
return (
(cubic_bezier_single_coordinate(p0[0], p1[0], p2[0], p3[0], t)),
(cubic_bezier_single_coordinate(p0[1], p1[1], p2[1], p3[1], t))
)
def random_anysign(a: int, b: int) -> int:
result = random.randint(a, b)
if random.randint(0, 1):
return -result
return result
def get_bezier_path(start: Point, end: Point, intermediate_radius_interval: tuple[int, int]=DEFAULT_INTERMEDIATE_RADIUS_INTERVAL) -> Callable[[float], Point]:
p0, p3 = start, end
p1 = (
p0[0]+random_anysign(intermediate_radius_interval[0], intermediate_radius_interval[1]),
p0[1]+random_anysign(intermediate_radius_interval[0], intermediate_radius_interval[1])
)
p2 = (
p3[0]+random_anysign(intermediate_radius_interval[0], intermediate_radius_interval[1]),
p3[1]+random_anysign(intermediate_radius_interval[0], intermediate_radius_interval[1])
)
return partial(cubic_bezier, p0, p1, p2, p3)
def get_distorted_bezier_path(
start: Point,
end: Point,
intermediate_radius_interval: tuple[int, int]=DEFAULT_INTERMEDIATE_RADIUS_INTERVAL,
distortion_zone_time_length: float=DEFAULT_DISTORTION_ZONE_TIME_LENGTH,
distortion_frequency: float=DEFAULT_DISTORTION_FREQUENCY,
deviation_interval: tuple[int, int]=DEFAULT_DEVIATION_INTERVAL
) -> Callable[[float], Point]:
distortion_zones: list[tuple[float, float]] = [
(i*distortion_zone_time_length, (i+1)*distortion_zone_time_length)
for i in range(int(1/distortion_zone_time_length))
if random.uniform(0, 1) < distortion_frequency
]
distortion_offsets: list[Point] = [
(
random_anysign(deviation_interval[0], deviation_interval[1]),
random_anysign(deviation_interval[0], deviation_interval[1])
)
for _ in range(len(distortion_zones))
]
def get_distorted_point(
true_point: Point,
distortion_offset: Point,
distortion_zone: tuple[float, float],
t: float
) -> Point:
distortion_zone_length = distortion_zone[1]-distortion_zone[0]
distortion_zone_progress = (t-distortion_zone[0])/distortion_zone_length
if distortion_zone_progress < 0.5: # move from true to distorted point
return (
true_point[0]+distortion_offset[0]*distortion_zone_progress*2,
true_point[1]+distortion_offset[1]*distortion_zone_progress*2
)
else: # move from distorted to true point
return (
true_point[0]+distortion_offset[0]*(1-(distortion_zone_progress-0.5)*2),
true_point[1]+distortion_offset[1]*(1-(distortion_zone_progress-0.5)*2)
)
bezier_path = get_bezier_path(start, end, intermediate_radius_interval)
def distored_path_function(t: float):
true_point = bezier_path(t)
for i, distortion_zone in enumerate(distortion_zones):
if distortion_zone[0] <= t <= distortion_zone[1]:
return get_distorted_point(
true_point,
distortion_offsets[i],
distortion_zone,
t
)
# we are not in a distortion zone, return the true point
return true_point
return distored_path_function
def logistic_sigmoid(x: float) -> float:
return 2/(1+np.exp(-x)) - 1
def get_path_with_transformed_velo(
start: Point,
end: Point,
intermediate_radius_interval: tuple[int, int]=DEFAULT_INTERMEDIATE_RADIUS_INTERVAL,
distortion_zone_time_length: float=DEFAULT_DISTORTION_ZONE_TIME_LENGTH,
distortion_frequency: float=DEFAULT_DISTORTION_FREQUENCY,
deviation_interval: tuple[int, int]=DEFAULT_DEVIATION_INTERVAL
) -> Callable[[float], Point]:
bezier_path = get_distorted_bezier_path(
start,
end,
intermediate_radius_interval,
distortion_zone_time_length,
distortion_frequency,
deviation_interval
)
return lambda t: bezier_path(logistic_sigmoid(t))
FITTS_LAW_A = 0.5500
FITTS_LAW_B = 0.1276
def get_final_path_from_real_time(
movement_time: float,
start: Point,
end: Point,
intermediate_radius_interval: tuple[int, int]=DEFAULT_INTERMEDIATE_RADIUS_INTERVAL,
distortion_zone_time_length: float=DEFAULT_DISTORTION_ZONE_TIME_LENGTH,
distortion_frequency: float=DEFAULT_DISTORTION_FREQUENCY,
deviation_interval: tuple[int, int]=DEFAULT_DEVIATION_INTERVAL
) -> Callable[[float], Point]:
path = get_path_with_transformed_velo(
start,
end,
intermediate_radius_interval,
distortion_zone_time_length,
distortion_frequency,
deviation_interval
)
def final_path_function(t: float) -> Point:
if t < 0:
return start
elif t > movement_time:
return end
normalized_t = (t / movement_time)*4.5
return path(normalized_t)
return final_path_function
def get_movement_time_from_fitts_law(distance: float, target_width: float) -> float:
index_of_difficulty = math.log2((2.0 * distance) / target_width)
movement_time = FITTS_LAW_A + FITTS_LAW_B * index_of_difficulty
return movement_time
def get_final_path_with_fitts_law(
target_width: float,
start: Point,
end: Point,
intermediate_radius_interval: tuple[int, int]=DEFAULT_INTERMEDIATE_RADIUS_INTERVAL,
distortion_zone_time_length: float=DEFAULT_DISTORTION_ZONE_TIME_LENGTH,
distortion_frequency: float=DEFAULT_DISTORTION_FREQUENCY,
deviation_interval: tuple[int, int]=DEFAULT_DEVIATION_INTERVAL
) -> Callable[[float], Point]:
distance = math.dist(start, end)
movement_time = get_movement_time_from_fitts_law(distance, target_width)
return get_final_path_from_real_time(
movement_time,
start,
end,
intermediate_radius_interval,
distortion_zone_time_length,
distortion_frequency,
deviation_interval
)
def choose_target_in_element(x: int, y: int, height: int, width: int) -> Point:
# choose a random point near the center of the element
left_bound_x = x + width * 0.25
right_bound_x = x + width * 0.75
top_bound_y = y + height * 0.25
bottom_bound_y = y + height * 0.75
return (
random.randint(int(left_bound_x), int(right_bound_x)),
random.randint(int(top_bound_y), int(bottom_bound_y))
)
class MouseUtils:
def __init__(self, driver: webdriver.Edge):
self.driver = driver
self.reinitialize()
def reinitialize(self):
self.init_driver_with_mouse_tracking()
self.init_driver_with_cursor_visualization()
def init_driver_with_mouse_tracking(self):
js_tracker = """
window.cursorX = 0;
window.cursorY = 0;
document.addEventListener('mousemove', function(event) {
console.log('Mouse moved to: ' + event.clientX + ', ' + event.clientY);
window.cursorX = event.clientX;
window.cursorY = event.clientY;
});
"""
self.driver.execute_script(js_tracker)
def init_driver_with_cursor_visualization(self):
cursor_script = """
var visualCursor = document.createElement('div');
visualCursor.id = 'selenium-visual-cursor';
visualCursor.style.position = 'fixed';
visualCursor.style.zIndex = '99999';
visualCursor.style.width = '15px';
visualCursor.style.height = '15px';
visualCursor.style.background = 'red';
visualCursor.style.borderRadius = '50%';
visualCursor.style.border = '2px solid white';
visualCursor.style.pointerEvents = 'none'; // Prevents blocking element clicks
visualCursor.style.top = '0px';
visualCursor.style.left = '0px';
visualCursor.style.transition = 'all 0.3s ease;'; // Optional: adds smooth sliding visual
document.body.appendChild(visualCursor);
window.moveVisualCursor = function(x, y) {
var cursor = document.getElementById('selenium-visual-cursor');
cursor.style.left = x + 'px';
cursor.style.top = y + 'px';
};
"""
self.driver.execute_script(cursor_script)
def get_current_mouse_position(self) -> Point:
x = self.driver.execute_script("return window.cursorX;")
y = self.driver.execute_script("return window.cursorY;")
return (x, y)
def move_mouse(self, move_time: float, path_function: Callable[[float], Point], visualize: bool=True):
start_time = time.monotonic()
end_time = start_time + move_time
while (current_time := time.monotonic()) < end_time:
t = current_time - start_time
point = path_function(t)
point = (max(0, point[0]), max(0, point[1])) # ensure the point is not negative
actions = ActionBuilder(self.driver, duration=0)
actions.pointer_action.move_to_location(point[0], point[1])
actions.perform()
if visualize: self.driver.execute_script(f"window.moveVisualCursor({point[0]}, {point[1]});")
def move_to_element(self, element: WebElement, visualize: bool=True):
current_mouse_position = self.get_current_mouse_position()
rect = self.driver.execute_script("""
var rect = arguments[0].getBoundingClientRect();
return {x: rect.left, y: rect.top, width: rect.width, height: rect.height};
""", element)
target_position = choose_target_in_element(
rect['x'],
rect['y'],
rect['height'],
rect['width']
)
move_time = get_movement_time_from_fitts_law(
math.dist(current_mouse_position, target_position),
(rect['width'] + rect['height']) / 2
)
path_fn = get_final_path_from_real_time(
movement_time=move_time,
start=current_mouse_position,
end=target_position
)
self.move_mouse(move_time, path_fn, visualize)
def human_like_click(self, time_interval: tuple[int, int]=(200, 300)):
ActionChains(self.driver, duration=random.randint(time_interval[0], time_interval[1])).click().perform()
+43
View File
@@ -0,0 +1,43 @@
import textwrap
import keyboard as kb
import pygetwindow as pygw
from matplotlib import pyplot as plt
from selenium import webdriver
from constants import USER_DATA_DIR, PROFILE_NAME
keypress_times: list[float] = []
def key_event_handler(event: kb.KeyboardEvent):
if event.event_type == kb.KEY_DOWN:
timestamp = event.time
key = event.name
window = pygw.getActiveWindow()
if window and "Edge" in window.title:
keypress_times.append(timestamp)
kb.hook(key_event_handler)
options = webdriver.EdgeOptions()
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option('useAutomationExtension', False)
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument(f"--user-data-dir={USER_DATA_DIR}")
options.add_argument(f"--profile-directory={PROFILE_NAME}")
driver = webdriver.Edge(options=options)
driver.get("https://rewards.bing.com/")
input("Press Enter to exit...")
press_time_differences = [t2 - t1 for t1, t2 in zip(keypress_times[:-1], keypress_times[1:])]
plt.hist(press_time_differences)
plt.savefig("keypress_times.png")
open("keypress_times.txt", "w").writelines(str(diff)+'\n' for diff in press_time_differences)
driver.quit()
+196
View File
@@ -0,0 +1,196 @@
import os
import random
import time
from typing import Callable
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.action_chains import ActionChains
import tab_utils
import llm_utils
import mouse_trajectory
import mimic_typing
import element_selectors
class RewardsTaskUtils:
def __init__(self, driver: webdriver.Edge):
self.driver = driver
self.tab_utils = tab_utils.TabUtils(driver)
self.mouse = mouse_trajectory.MouseUtils(driver)
self.keyboard = mimic_typing.KeyboardUtils(driver)
self.elements = element_selectors.ElementSelectionUtils(driver)
def find_element(self, xpath: str):
return self.driver.find_element(By.XPATH, xpath)
def wait_for_element(self, element_getter: Callable[[], WebElement | list[WebElement]], timeout: int = 10) -> WebElement | list[WebElement]:
def condition(_: webdriver.Edge):
try:
element_or_elements = element_getter()
return element_or_elements
except:
return False
return WebDriverWait(self.driver, timeout).until(condition)
def switch_to_earn_page(self):
self.move_to_and_click(self.elements.get_earn_tab())
def switch_to_dashboard(self):
self.move_to_and_click(self.elements.get_dashboard_tab())
def move_to_and_click(self, elem: WebElement):
self.mouse.move_to_element(elem)
self.mouse.human_like_click()
def wait_for_then_click(self, element_getter: Callable[[], WebElement], timeout: int = 10):
elem = self.wait_for_element(element_getter, timeout)
self.move_to_and_click(elem)
def complete_bing_daily_set(self):
self.switch_to_earn_page()
self.wait_for_then_click(self.elements.get_open_daily_set_button)
daily_set_links = self.wait_for_element(self.elements.get_daily_set_elements)
self.move_to_and_click(daily_set_links[0])
time.sleep(random.uniform(2, 3))
self.driver.switch_to.window(self.driver.current_window_handle) # refocus on the main tab
self.move_to_and_click(daily_set_links[1])
time.sleep(random.uniform(2, 3))
self.driver.switch_to.window(self.driver.current_window_handle)
self.move_to_and_click(daily_set_links[2])
time.sleep(random.uniform(2, 3))
self.driver.switch_to.window(self.driver.current_window_handle)
self.tab_utils.close_all_other_tabs()
def complete_explore_on_bing_tasks(self):
self.switch_to_earn_page()
explore_on_bing_links = self.wait_for_element(self.elements.get_explore_on_bing_elements)
for card in explore_on_bing_links:
desc = self.elements.extract_card_descriptions(card)
query = llm_utils.get_search_query_from_task_description(desc)
self.move_to_and_click(card)
self.tab_utils.switch_to_other_tab()
self.wait_for_element(self.elements.get_bing_search_bar)
# search bar should be auto-focused
self.keyboard.send_keys(query+Keys.ENTER)
time.sleep(random.uniform(2, 3))
self.tab_utils.switch_to_other_tab()
self.tab_utils.close_all_other_tabs()
for card in explore_on_bing_links:
if not self.elements.card_is_complete(card):
print(f"WARNING: Explore on Bing Card [desc={self.elements.extract_card_descriptions(card)!r}] is not complete after searching. Please check manually.")
def complete_visual_search(self):
self.switch_to_earn_page()
self.wait_for_then_click(self.elements.get_open_visual_search_sidebar)
self.wait_for_then_click(self.elements.get_search_now_link_from_visual_search_sidebar)
self.tab_utils.switch_to_other_tab()
self.mouse.reinitialize()
self.wait_for_then_click(self.elements.get_visual_search_button)
file_input = self.wait_for_element(self.elements.get_visual_search_file_input)
file_input.send_keys(os.path.abspath("keypress_times.png"))
time.sleep(random.uniform(3, 5))
self.tab_utils.switch_to_other_tab()
self.tab_utils.close_all_other_tabs()
self.mouse.reinitialize()
def complete_misc_cards(self):
self.switch_to_earn_page()
misc_cards: list[WebElement] = self.wait_for_element(self.elements.get_all_misc_cards)
for card in misc_cards:
while not self.elements.element_is_fully_in_viewport(card): # this should work for top-down iteration
ActionChains(self.driver).scroll_by_amount(0, 100).perform()
if not self.elements.card_is_complete(card) and self.elements.get_card_point_value(card) > 0:
self.move_to_and_click(card)
time.sleep(random.uniform(1, 2))
self.driver.switch_to.window(self.driver.current_window_handle)
for card in misc_cards:
if not self.elements.card_is_complete(card) and self.elements.get_card_point_value(card) > 0:
print(f"WARNING: Misc Card [desc={self.elements.extract_card_descriptions(card)!r}] is not complete after clicking. Please check manually.")
self.tab_utils.close_all_other_tabs()
def complete_twenty_searches(self):
self.driver.get("https://www.bing.com/")
search_bar = self.wait_for_element(self.elements.get_bing_search_bar)
# search bar should be auto-focused
for query in llm_utils.get_related_search_queries(
llm_utils.get_random_noun(), num_queries=20
):
self.keyboard.send_keys(query+Keys.ENTER)
time.sleep(random.uniform(2, 3))
# clear search bar for next query
# the 't' can be any character, it just needs to be there to
# auto-focus the search bar so that the backspaces will work
self.keyboard.send_keys('t'+Keys.BACKSPACE*(len(query)+1))
self.driver.get("https://rewards.bing.com/")
self.mouse.reinitialize()
self.switch_to_earn_page()
self.wait_for_then_click(self.elements.get_points_breakdown_button)
close_btn = self.wait_for_element(self.elements.get_close_button_on_points_breakdown)
points_earned, max_pts = self.elements.get_points_earned_from_searches_on_points_breakdown()
self.move_to_and_click(close_btn)
print(f"Points earned from 20 searches: {points_earned}/{max_pts}")
def claim_bonus_points(self):
self.switch_to_dashboard()
self.wait_for_then_click(self.elements.get_bonus_button_on_dashboard)
try:
self.move_to_and_click(self.elements.get_claim_bonus_points_button())
except IndexError:
print("[WARNING] Could not find the 'Claim Bonus Points' button. There are likely no bonus points to claim at this time.")
def complete_all_tasks(self):
self.complete_bing_daily_set()
self.complete_explore_on_bing_tasks()
self.complete_visual_search()
self.complete_misc_cards()
self.complete_twenty_searches()
self.claim_bonus_points()
+33
View File
@@ -0,0 +1,33 @@
from selenium.common.exceptions import WebDriverException
from selenium import webdriver
class TabUtils:
def __init__(self, driver: webdriver.Edge):
self.driver = driver
self.problematic_tabs = set()
def switch_to_other_tab(self):
current_window = self.driver.current_window_handle
for handle in self.driver.window_handles:
if handle != current_window and handle not in self.problematic_tabs:
self.driver.switch_to.window(handle)
return
def close_all_other_tabs(self, exceptions: list[str] = None):
if exceptions is None:
exceptions = [self.driver.current_window_handle]
switch_back_to = exceptions[0]
for handle in self.driver.window_handles:
if handle not in exceptions and handle not in self.problematic_tabs:
self.driver.switch_to.window(handle)
try: self.driver.close()
except WebDriverException:
print(f"[WARNING] Could not close tab with handle {handle}.")
self.problematic_tabs.add(handle)
pass
self.driver.switch_to.window(switch_back_to)
+160
View File
@@ -0,0 +1,160 @@
import math
import random
import time
import pygame
from mouse_trajectory import get_final_path_with_fitts_law, get_movement_time_from_fitts_law
WINDOW_WIDTH = 1100
WINDOW_HEIGHT = 760
BACKGROUND = (248, 248, 248)
START_COLOR = (25, 25, 25)
TARGET_FILL = (75, 136, 255)
TARGET_OUTLINE = (18, 54, 120)
DOT_COLOR = (220, 60, 60)
PATH_COLOR = (110, 110, 110)
BUTTON_BG = (30, 30, 30)
BUTTON_TEXT = (255, 255, 255)
class Target:
def __init__(self, x: int, y: int, width: int, height: int):
self.x = x
self.y = y
self.width = width
self.height = height
@property
def center(self) -> tuple[int, int]:
return (int(self.x + self.width / 2), int(self.y + self.height / 2))
@property
def effective_width(self) -> float:
return (self.width + self.height) / 2.0
def random_point_inside(self) -> tuple[int, int]:
px = random.randint(self.x, self.x + self.width)
py = random.randint(self.y, self.y + self.height)
return (px, py)
def rect(self) -> pygame.Rect:
return pygame.Rect(self.x, self.y, self.width, self.height)
def generate_target(start: tuple[int, int], margin: int = 70) -> Target:
width = random.randint(30, 180)
height = random.randint(30, 180)
attempts = 0
while attempts < 2000:
x = random.randint(margin, WINDOW_WIDTH - width - margin)
y = random.randint(margin, WINDOW_HEIGHT - height - margin)
target = Target(x, y, width, height)
center = target.center
if math.dist(start, center) > 150:
return target
width = random.randint(30, 180)
height = random.randint(30, 180)
attempts += 1
return Target(WINDOW_WIDTH - width - 120, WINDOW_HEIGHT // 2, width, height)
def draw_path_trace(screen: pygame.Surface, path_fn, movement_time: float, steps: int = 320) -> None:
points = []
for i in range(steps):
sample_time = movement_time * (i / (steps - 1))
x, y = path_fn(sample_time)
points.append((int(x), int(y)))
if len(points) > 1:
pygame.draw.lines(screen, PATH_COLOR, False, points, 2)
def main() -> None:
pygame.init()
screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption("Fitts Law Target Acquisition")
clock = pygame.time.Clock()
font = pygame.font.SysFont("Segoe UI", 20)
small_font = pygame.font.SysFont("Segoe UI", 18)
start_position = (150, 520)
current_position = start_position
current_target = generate_target(current_position)
current_end = current_target.random_point_inside()
target_width = current_target.effective_width
move_start_time = time.monotonic()
path_fn = get_final_path_with_fitts_law(target_width, current_position, current_end)
movement_time = get_movement_time_from_fitts_law(math.dist(current_position, current_end), target_width)
state = "moving"
replay_button = pygame.Rect(580, 40, 220, 54)
next_button = pygame.Rect(820, 40, 220, 54)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
return
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
if state == "waiting":
if replay_button.collidepoint(event.pos):
current_position = start_position
move_start_time = time.monotonic()
state = "moving"
elif next_button.collidepoint(event.pos):
current_position = start_position
current_target = generate_target(current_position)
current_end = current_target.random_point_inside()
target_width = current_target.effective_width
move_start_time = time.monotonic()
path_fn = get_final_path_with_fitts_law(target_width, current_position, current_end)
movement_time = get_movement_time_from_fitts_law(math.dist(current_position, current_end), target_width)
state = "moving"
screen.fill(BACKGROUND)
if state == "moving":
elapsed = time.monotonic() - move_start_time
current_position = path_fn(elapsed)
if elapsed >= movement_time:
current_position = current_end
state = "waiting"
target_rect = current_target.rect()
pygame.draw.rect(screen, TARGET_FILL, target_rect, border_radius=6)
pygame.draw.rect(screen, TARGET_OUTLINE, target_rect, 3, border_radius=6)
draw_path_trace(screen, path_fn, movement_time)
pygame.draw.circle(screen, DOT_COLOR, (int(current_position[0]), int(current_position[1])), 8)
if state == "waiting":
pygame.draw.rect(screen, BUTTON_BG, replay_button, border_radius=10)
replay_text = font.render("Replay", True, BUTTON_TEXT)
screen.blit(replay_text, (replay_button.x + 68, replay_button.y + 12))
pygame.draw.rect(screen, BUTTON_BG, next_button, border_radius=10)
button_text = font.render("Next target", True, BUTTON_TEXT)
screen.blit(button_text, (next_button.x + 45, next_button.y + 12))
prompt = small_font.render("Target reached. Replay or continue to next target.", True, (30, 30, 30))
screen.blit(prompt, (35, 35))
label = small_font.render(
f"Target W = (H + W)/2 = {target_width:.1f}px D = {math.dist(start_position, current_end):.1f}px",
True,
(35, 35, 35),
)
screen.blit(label, (30, 95))
pygame.display.flip()
clock.tick(60)
if __name__ == "__main__":
main()