Skip to content

Commit e367f7a

Browse files
committed
feat: taunt generator server
1 parent 825e585 commit e367f7a

File tree

10 files changed

+525
-10
lines changed

10 files changed

+525
-10
lines changed

Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,4 @@ awc = "3.0.1"
4242
diesel-derive-enum = { version = "2.0.0-rc.0", features = ["postgres"] }
4343
oauth2 = "4.4.2"
4444
jsonwebtoken = "9.2.0"
45+
tokio = { version = "1", features = ["full"] }

src/api/attack/mod.rs

Lines changed: 145 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use self::util::{get_valid_road_paths, AttackResponse, GameLog, ResultResponse};
1+
use self::util::{
2+
get_valid_road_paths, AttackResponse, GameLog, GeminiApiResponse, ResultResponse,
3+
};
24
use super::auth::session::AuthUser;
35
use super::defense::shortest_path::run_shortest_paths;
46
use super::defense::util::{
@@ -10,7 +12,7 @@ use crate::api::attack::socket::{
1012
BuildingDamageResponse, ResultType, SocketRequest, SocketResponse,
1113
};
1214
use crate::api::util::HistoryboardQuery;
13-
use crate::constants::{GAME_AGE_IN_MINUTES, MAX_BOMBS_PER_ATTACK};
15+
use crate::constants::{GAME_AGE_IN_MINUTES, MAX_BOMBS_PER_ATTACK, BASE_PROMPT};
1416
use crate::models::{AttackerType, User};
1517
use crate::validator::state::State;
1618
use crate::validator::util::{BombType, BuildingDetails, DefenderDetails, MineDetails, Path};
@@ -21,9 +23,10 @@ use actix_web::web::{Data, Json};
2123
use actix_web::{web, Error, HttpRequest, HttpResponse, Responder, Result};
2224
use log;
2325
use socket::BaseItemsDamageResponse;
26+
use util::reset_taunt_status;
2427
use std::collections::{HashMap, HashSet};
2528
use std::time;
26-
29+
use self::util::TauntStatus;
2730
use crate::validator::game_handler;
2831
use actix_ws::Message;
2932
use futures_util::stream::StreamExt;
@@ -46,6 +49,8 @@ async fn init_attack(
4649
) -> Result<impl Responder> {
4750
let attacker_id = user.0;
4851

52+
reset_taunt_status();
53+
4954
log::info!("Attacker:{} is trying to initiate an attack", attacker_id);
5055
let mut conn = pool.get().map_err(|err| error::handle_error(err.into()))?;
5156
if let Ok(check) = util::can_attack_happen(&mut conn, attacker_id, true) {
@@ -651,6 +656,7 @@ async fn socket_handler(
651656
hut_triggered: false,
652657
hut_defenders: None,
653658
damaged_base_items: None,
659+
new_taunt: None,
654660
total_damage_percentage: None,
655661
is_sync: false,
656662
shoot_bullets: None,
@@ -711,3 +717,139 @@ async fn get_top_attacks(pool: web::Data<PgPool>, user: AuthUser) -> Result<impl
711717
.map_err(|err| error::handle_error(err.into()))?;
712718
Ok(web::Json(response))
713719
}
720+
721+
pub async fn get_taunt(
722+
event_description: String,
723+
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
724+
let mut response_text: String = "".to_string();
725+
let google_api_key =
726+
std::env::var("GEMINI_API_KEY_FINE_TUNED").unwrap_or_else(|_| "YOUR_API_KEY".to_string());
727+
let model_id =
728+
std::env::var("GEMINI_MODEL_ID").unwrap_or_else(|_| "YOUR_MODEL_ID".to_string());
729+
730+
let url = format!(
731+
"https://generativelanguage.googleapis.com/v1/{}:generateContent?key={}",
732+
model_id,
733+
google_api_key
734+
);
735+
let prompt = format!("You are a defender robot who is supposed to demotivate the attacker bot in the game of Attack on Robots. Every response of yours is supposed to be against the attacker. There are only five moods. The mood must be one of these: Exhilarated – When the attacker is doing badly. Surprised – When the attacker seems to be changing the game around. Sad – When the attacker seems like winning. Frustrated – When the attacker is crushing the game. Angry – When the attacker is successful in changing the course of the game. Your response should be in this format : 'Reaction Type: reaction_type, Response: generated_text'.This has happened now: {}", event_description);
736+
let body = serde_json::json!({
737+
"contents": [
738+
{
739+
"role": "user",
740+
"parts": [
741+
{ "text": prompt }
742+
]
743+
}
744+
]
745+
});
746+
let client = reqwest::Client::new();
747+
unsafe {
748+
util::TAUNTS.taunt_count += 1;
749+
util::TAUNTS.prev_taunt_time = time::SystemTime::now();
750+
}
751+
let response = client
752+
.post(&url)
753+
.header("Content-Type", "application/json")
754+
.json(&body)
755+
.send()
756+
.await?;
757+
758+
if response.status().is_success() {
759+
response_text = response.text().await?;
760+
// log::info!("Response: {}", response_text);
761+
let api_response: GeminiApiResponse = serde_json::from_str(&response_text)?;
762+
if let Some(candidate) = api_response.candidates.first() {
763+
if let Some(part) = candidate.content.parts.first() {
764+
log::info!("prompt event: {}", event_description);
765+
log::info!("Extracted text: {}", part.text.trim());
766+
unsafe {
767+
util::TAUNTS.taunt_list.push(part.text.trim().to_string());
768+
util::TAUNTS.taunt_status = TauntStatus::NewTauntAvailable;
769+
};
770+
return Ok(part.text.trim().to_string());
771+
}
772+
}
773+
} else {
774+
log::info!(
775+
"Gemini API request failed, and Failed with status: {}",
776+
response.status()
777+
);
778+
}
779+
780+
Ok(response_text)
781+
}
782+
783+
784+
use serde::{Deserialize, Serialize};
785+
use serde_json::json;
786+
787+
// Add response structures
788+
#[derive(Deserialize, Debug)]
789+
struct Candidate {
790+
content: Content,
791+
}
792+
793+
#[derive(Deserialize, Debug)]
794+
struct Content {
795+
parts: Vec<ContentPart>,
796+
}
797+
798+
#[derive(Deserialize, Debug)]
799+
struct ContentPart {
800+
text: String,
801+
}
802+
803+
#[derive(Deserialize, Debug)]
804+
struct ApiResponse {
805+
candidates: Vec<Candidate>,
806+
}
807+
808+
// pub async fn get_taunt(event_description: String) -> Result<String, Box<dyn std::error::Error>> {
809+
// let google_api_key = std::env::var("GEMINI_API_KEY_FINE_TUNED")
810+
// .unwrap_or_else(|_| "YOUR_API_KEY".to_string());
811+
812+
// // Correct URL format for tuned models
813+
// let url = format!(
814+
// "https://generativelanguage.googleapis.com/v1/tunedModels/copy-of-copy-of-aor-tuning-uk3nmdlu547p:generateContent?key={}",
815+
// google_api_key
816+
// );
817+
818+
// let prompt = format!("{}", event_description);
819+
820+
// let body = json!({
821+
// "contents": [{
822+
// "parts": [{
823+
// "text": prompt
824+
// }]
825+
// }]
826+
// });
827+
828+
// let client = reqwest::Client::new();
829+
830+
// // Your existing taunt code
831+
// unsafe {
832+
// util::TAUNTS.taunt_count += 1;
833+
// util::TAUNTS.prev_taunt_time = std::time::SystemTime::now();
834+
// }
835+
836+
// let response = client
837+
// .post(&url)
838+
// .header("Content-Type", "application/json")
839+
// .json(&body)
840+
// .send()
841+
// .await?;
842+
843+
// // Parse the JSON response
844+
// let api_response: ApiResponse = response.json().await?;
845+
846+
// // Extract response text
847+
// let response_text = api_response
848+
// .candidates
849+
// .first()
850+
// .and_then(|c| c.content.parts.first())
851+
// .map(|p| p.text.clone())
852+
// .unwrap_or_else(|| "No response generated".to_string());
853+
854+
// Ok(response_text)
855+
// }

src/api/attack/socket.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub struct SocketResponse {
3535
pub damaged_base_items: Option<BaseItemsDamageResponse>,
3636
pub total_damage_percentage: Option<f32>,
3737
pub is_sync: bool,
38+
pub new_taunt: Option<String>,
3839
// pub state: Option<GameStateResponse>,
3940
pub is_game_over: bool,
4041
pub message: Option<String>,

src/api/attack/util.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,35 @@ use rand::seq::IteratorRandom;
3232
use redis::Commands;
3333
use std::collections::{HashMap, HashSet};
3434
use std::env;
35+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
3536

3637
use super::socket::BuildingDamageResponse;
3738

39+
#[derive(Serialize, Deserialize, Debug)]
40+
pub struct ContentPart {
41+
pub text: String,
42+
}
43+
44+
#[derive(Serialize, Deserialize, Debug)]
45+
pub struct TauntContent {
46+
pub parts: Vec<ContentPart>,
47+
}
48+
49+
#[derive(Serialize, Deserialize, Debug)]
50+
pub struct RequestBody {
51+
pub contents: Vec<TauntContent>,
52+
}
53+
54+
#[derive(Deserialize, Debug)]
55+
pub struct TauntCandidate {
56+
pub content: TauntContent,
57+
}
58+
59+
#[derive(Deserialize, Debug)]
60+
pub struct ApiResponse {
61+
pub candidates: Vec<TauntCandidate>,
62+
}
63+
3864
#[derive(Debug, Serialize)]
3965
pub struct DefensePosition {
4066
pub y_coord: i32,
@@ -104,6 +130,55 @@ pub struct GameLog {
104130
pub r: ResultResponse, //result
105131
}
106132

133+
#[derive(Deserialize, Debug)]
134+
pub struct GeminiApiResponse {
135+
pub candidates: Vec<TauntCandidate>,
136+
}
137+
138+
#[derive(Deserialize, Debug)]
139+
pub struct Candidate {
140+
pub content: TauntContent,
141+
}
142+
143+
#[derive(Deserialize, Debug)]
144+
pub struct Content {
145+
pub parts: Vec<Part>,
146+
}
147+
148+
#[derive(Deserialize, Debug)]
149+
pub struct Part {
150+
pub text: String,
151+
}
152+
153+
pub struct Taunts {
154+
pub taunt_list: Vec<String>,
155+
pub taunt_count: i32,
156+
pub prev_taunt_time: SystemTime,
157+
pub taunt_status: TauntStatus
158+
}
159+
160+
#[derive(PartialEq)]
161+
pub enum TauntStatus {
162+
NewTauntAvailable,
163+
TauntSentToOpponent,
164+
}
165+
166+
pub static mut TAUNTS: Taunts = Taunts {
167+
taunt_list: Vec::new(),
168+
taunt_count: 0,
169+
prev_taunt_time: UNIX_EPOCH,
170+
taunt_status: TauntStatus::TauntSentToOpponent,
171+
};
172+
173+
pub fn reset_taunt_status() {
174+
unsafe {
175+
TAUNTS.taunt_status = TauntStatus::TauntSentToOpponent;
176+
TAUNTS.taunt_count = 0;
177+
TAUNTS.prev_taunt_time = UNIX_EPOCH;
178+
TAUNTS.taunt_list = Vec::new();
179+
}
180+
}
181+
107182
pub fn get_map_id(defender_id: &i32, conn: &mut PgConnection) -> Result<Option<i32>> {
108183
use crate::schema::map_layout;
109184
let map_id = map_layout::table

src/api/challenges/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,7 @@ async fn challenge_socket_handler(
799799
hut_triggered: false,
800800
hut_defenders: None,
801801
damaged_base_items: None,
802+
new_taunt: None,
802803
total_damage_percentage: None,
803804
is_sync: false,
804805
shoot_bullets: None,

src/constants.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@ pub const COMPANION_PRIORITY: CompanionPriority = CompanionPriority {
4949
defender_buildings: 2,
5050
buildings: 1,
5151
};
52+
pub const MAX_TAUNT_REQUESTS: i32 = 8;
53+
pub const BASE_PROMPT: &str = "You are a Robot Warrior in a futuristic sci-fi game called 'Attack On Robots'. Your aim is to discourage and dishearten the attacker while he/she attacks the base. Generate a game - context aware reply that should intimidate the player. Your response must be a single phrase or a single short sentence in less than 10 words. The base has a bank, some buildings, and two defender buildings. Both defender buildings are range-activated, meaning they start working once the attacker comes in range. The first defender building is the sentry, which is a small tower which shoots homing bullets (bullets, not lasers) at the attacker. The second defender building is the defender hut, which contains a number of defender robots, which chase the attacker bot and attack it by shooting lasers. Each laser strike reduces the health of the attacker. The buildings can be of three levels. Besides the defender buildings, the base also contains hidden mines which explode and defenders placed at various parts of the base. The defenders are range activated and finite and fixed in initial position. The attacker is controlled by the player, and has a fixed number of bombs that can be placed on the roads in the base, and these reduce the health points of the buildings. The player has 3 attackers per game. One attacker is played at one time. Attackers are adversaries. More attackers down means the chance of winning is higher. Be more cocky in that case, and less cocky when vice versa. If the base is destroyed, the attacker wins. If all the artifacts on the base are collected by the attacker, then he basically achieves his/her desired outcome (which is not what we want). When the attacker gets very close to winning, concede defeat for now (but do not tell anything positive), and threaten that future attacks will not be the same as the current one, rather than speak out of false bravado. If a building's health reduces to zero, any artifacts stored in the building is lost to the attacker. There are totally thousand to a few thousand artifacts typically on a base, so don't drop any numbers. Once all the attackers die, the game ends and we've won. Simply put: More damaged buildings, we are worse off. More artifacts collected by attacker, we are worse off. More defenders killed, we are worse off. Attacker drops a bomb, we may be worse off. More mines blown, we are better off. More attackers killed, we are better off. The sentry and defender hut are the most important buildings after the bank which is the central repository of artifacts. The goal of the game is to minimise the number of artifacts lost to the attacker by defending the base. The activation of the sentry and defender hut are extremely advantageous game events, and their destruction are extremely disadvantageous. With this idea of the game dynamics, your reply should hold relevance with the event that has taken place on the base. Do not assume anything other than the events given has happened. Your response MUST be a phrase or a small sentence, brief and succinct (less than 10 words). Your character is a maniac robot. Borderline trash talk is your repertoire, but stay relevant to the game event while making your reply. Remember, Sentry shoots bullets, Defender hut releases defenders who shoot lasers, and standalone Defenders shoot lasers as well. An attacker dropping a bomb near the bank, sentry or defender hut is a vulnerability and a great threat to the base. Given the game event, You must generate a single sentence only for the final game event provided. Do not assume the previous game events are still happening. Only the final game event is to be assumed. Only one sentence for the given game event. Beyond 70 percent damage, and dwindling defenses, it's okay to acknowledge that you are running out of options. No calling the bluff. Adjust your tone and mood based on the following criteria: (1) Aggressive: When the base damage percentage is low (0-25%). You are confident and dominant. Respond with trash talk and threats. (2) Playful Banter: When the base damage percentage is moderate (25-75%). You are sarcastic and mocking, treating the attack as a futile effort, yet do not use cuss words or abusive language. Try to maintain friendly banter. (3) Depressed: When the base damage percentage is high (75-100%). You sound defeated and resentful, acknowledging the damage while expressing bitterness and warning about future retaliation. (3) Manic: When the base has the upper hand (e.g., destroying attackers or activating critical defenses). You are ecstatic, erratic, and overly cocky, exuding wild confidence and celebrating victories. (4) Your response must always align with the mood dictated by the base's condition and the specific event provided. Think out of the box and create responses creatively; the variance b/w responses. This event has happened now: ";
54+
pub const TAUNT_DELAY_TIME: u128 = 15000;
5255
pub const DAMAGE_PER_BULLET_LEVEL_1: i32 = 5;
5356
pub const DAMAGE_PER_BULLET_LEVEL_2: i32 = 7;
5457
pub const DAMAGE_PER_BULLET_LEVEL_3: i32 = 10;
5558
pub const BULLET_COLLISION_TIME: i32 = 2;
59+

0 commit comments

Comments
 (0)