mirror of
https://github.com/User0332/rewards-farmer.git
synced 2026-09-16 01:31:36 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
*.png
|
||||
*.txt
|
||||
Todo.md
|
||||
data-dir/
|
||||
__pycache__/
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"python-envs.defaultEnvManager": "ms-python.python:poetry",
|
||||
"python-envs.defaultPackageManager": "ms-python.python:poetry",
|
||||
}
|
||||
Generated
+5047
File diff suppressed because one or more lines are too long
@@ -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
|
||||
@@ -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}%)")
|
||||
@@ -0,0 +1,4 @@
|
||||
from os.path import abspath
|
||||
|
||||
USER_DATA_DIR = abspath("./data-dir")
|
||||
PROFILE_NAME = "Profile 1"
|
||||
@@ -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]
|
||||
@@ -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()
|
||||
@@ -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
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user