In the Age of Prostagma and Hacking
Ευάγγελος Γκαραγκάνης - 20 Σεπτεμβρίου 2024
Σε αυτό το άρθρο θα κάνουμε ένα ταξίδι νοσταλγίας και προγραμματισμού στο θρυλικό παιχνίδι των παιδικών μας χρόνων, Age of Mythology. Υπό τη σκία του νεοφερμένου Retold, αποφάσισα να σας διηγηθώ μια σύντομη ιστορία αρκουδοχακιών που συνέβη στην αρχή του έτους μας.
Για αρχή, γνωρίστε το Skelo. Ο Skelo είναι ο αιώνιος αντίπαλος μου στο Age of Mythology και μαζί με λίγους ακόμη πιστούς κρατάμε τη φλόγα του ζωντανή, 22 χρόνια μετά τη πρώτη του κυκλοφορία. Με το Skelo έχουμε παίξει μια ντουζίνα από επικά παιχνίδια στο παρελθόν, που έφταναν και τους δύο μας στα όρια μας. Τη τελευταία μέρα του 2023 οι 12 θέοι του AOM μας χάρισαν το επικέστερο παιχνίδι μας μέχρι και σήμερα το οποίο οδήγησε σε μια γλυκόπικρη ήττα. Παρά την ήττα μου, αυτό που με ενοχλούσε περισσότερο είναι ότι τόσες παιχνιδάρες μεταξύ μας δεν είχαν καταγραφεί τόσο για περεταίρω ανάλυση όσο και για να τις καμαρώσει το υπόλοιπο community. Τουλάχιστον αυτό νόμιζα αρχικά.
Μετά από μια γρήγορη επικοινωνία με το Skelo, αποδείχθηκε ότι είχε όλα τα παιχνίδια του καταγεγραμμένα, συμπεριλαμβανομένων και των δικών μας. Το πρόβλημα ήταν όμως ότι δεν είχε κάποιο τρόπο για να τα ψάξει. Το γεγονός όμως ότι υπήρχε έστω και η παραμικρή ελπίδα να τα βρω, με έκανε να τρέφω ελπίδες ότι ίσως υπήρχε κάποιος τρόπος. Ζήτησα λοιπόν από το Skelo να μου στείλει τα παιχνίδια του και συνέχισα με τους εορτασμούς της πρωτοχρονιάς.
Το 2024 ξεκινάει φρέσκο και με καλωσορίζει με ένα όμορφο hangover και μια ακόμη πιο όμορφη γρίπη. Έχοντας άπλετο χρόνο στο κρεβάτι, σκέφτηκα να ψάξω τα παιχνίδια μου με το Skelo στο φάκελο που μου έστειλε. Οι ελπίδες μου όμως ελαχιστοποιήθηκαν, αντικρίζοντας μπροστά μου 3000 μυστήρια rcx binary αρχεία τα οποία μπορούσε να ανοίξει ένα-ένα μόνο ο "περιηγητής" του AOM. Ήλπιζα ότι ανοίγοντας τα θα έβρισκα κάποια μεταδεδομένα για τις λεπτομέρειες του παιχνιδού, όπως για παράδειγμα ποιοί παίχτες έπαιζαν στο τρέχον παιχνίδι - τρομάρα μου. Δεδομένου ότι τα αρχεία ήταν binary και ο τύπος τους καμιά 20ρια χρόνια παλιός, υπήρχαν μηδαμινές πληροφορίες τόσο σε αυτά όσο και για αυτά στο internet. Δυστυχώς η μόνη εμφανής επιλογή, εκτός και αν έχετε να μου προτείνετε κάποια καλύτερα λύση, ήταν να δώ ένα-ένα τα 3000 παιχνίδια προκειμένου να βρώ τα δικά μου απέναντι στο Skelo και να τα αρχειοθετήσω.
Όπως καταλαβαίνετε η τρέλα μου για το παιχνίδι δεν είναι τόσο μεγάλη για να δω και να αρχειοθετήσω ένα-ένα 3000 παιχνίδια. Και για να είμαστε και ειλικρινείς, ποιος άνθρωπος με σώας τα φρένας θα το έκανε αυτό;
...
Τι και αν, όμως, δεν ήταν άνθρωπος;
...
Τι και αν έβαζα ένα robot να δει όλα τα παινδίδια και να βρεί τα δικά μου εναντίον του Skelo; Η ιδέα ήταν απλή και φάνταζε ένα ωραίο task αυτοματισμού. Ας πάρουμε, λοιπόν, τα πράγματα με τη σειρά και ας δούμε τι θα έπρεπε να κάνει το robot μας.
To robot θα έπρεπε να κάνει ακριβώς ότι θα έκανε και ένας άνθρωπος, δηλαδή:
- Να μετακινεί το ποντίκι και να πληκτρολογεί, προκειμένου να ανοίγει παράθυρα και να γράφει τα ονόματα των καταγεγραμένων παιχνιδιών.
- Να παρακολουθεί το παιχνίδι και να μπορεί να αναγνωρίζει ποιοί παίχτες παίζουν.
- Τέλος να αρχειοθετεί τις καταγραφές βάσει των δύο παικτών.
Δεδομένου ότι εκείνες τις ημέρες έγραφα κάποια JS προγράμματα, αποφάσισα να υλοποιήσω το robot επίσης σε Javascript. Έψαξα τις κατάλληλες βιβλιοθήκες για να δώσω στο robot μας τις παραπάνω δυνατότητες και αναλυτικά έχουμε:
-
Τα χέρια του robot, καθώς και τη δυνατότητα να κινεί το ποντίκι και να πλητρολογεί, προκειμένου να ανοίγει παράθυρα και να πληκτρολογεί το όνομα της καταγραφής, θα ήταν η βιβλιοθήκη αυτοματισμού RobotJS.
-
Τα μάτια του robot, καθώς και τη δυνατότητα να παρακολουθεί το παιχνίδι και να εστιάζει σε συγκεκριμένα σημεία, θα ήταν η βιβλιοθήκη Screenshot-desktop και η Sharp. Μέσω της screenshot-desktop βιβλιοθήκης θα μπορούμε να πάρουμε στιγμιότυπα της οθόνης και μέσω της sharp να "κόβουμε" τα σημεία που επιθυμούμε.
-
Τη ικανότητα ανάγνωσης για το robot μας, προκειμένου να διαβάζει τους παίχτες που παίζουν, θα την έδινε η βιβλιοθήκη αναγνώρισης κειμένου από εικόνα Tesseractjs.
Νομίζω ότι πλέον είναι ξεκάθαρο για το που πάει το πράγμα. Απλά να φτιάξουμε ένα πρόγραμμα που θα κάνει ακριβώς ότι θα έκανα και εγώ. Ας δούμε τώρα τη φάση της υλοποιήσης. Ο κώδικας του άρθρου βρίσκεται εδώ, μαζί με ένα μικρό αρχείο δεδομένων που περιέχει 3 rcx αρχεία.
async function automateRecordingsArchivalProcess() {
const folderPath = "./haki_games_small_subset";
const files = await fs.promises.readdir(
folderPath,
(err) => err && console.error(err)
);
for (const fileName of files) {
console.log(`🔴 Archiving recording with name ${fileName}`);
await automateRecordingArchivalProcess(fileName);
}
}
automateRecordingArchivalProcess();
Η συνάρτηση automateRecordingArchivalProcess θα πραγματοποιήσει την αρχειοθέτηση της κάθε εγγραφής στο σωστό σημείο, με το να παίρνει το όνομα του αρχείο εγγραφής.
async function automateRecordingArchivalProcess(recordingName) {
moveCursorToTextInputAndTypeRecordingName(recordingName);
openTheRecording();
openTheDropdownMenuWithUsernames();
await takeScreenshotAndCropUsernamesPart();
const usernamesRecognized = await tesseractRecognizeUsernamesInScreenshot();
moveRecordingToFolderForThoseUserMatches(usernamesRecognized);
openWindowToWatchAnotherRecording();
}
Το robot ξεκινάει με το μετακινεί το ποντίκι στο σημείο πληκτρολόγησης του ονόματος εγγραφής, να πληκτρολογεί το όνομα του αρχείου και μετά να ανοίγει την εγγραφή:
function moveCursorToTextInputAndTypeRecordingName(recordingName) {
console.log("...Moving cursor to text input and typing recording name...");
const TEXT_INPUT_LOCATION = [816, 699];
robot.moveMouse(TEXT_INPUT_LOCATION[0], TEXT_INPUT_LOCATION[1]);
robot.mouseClick();
const dateTimeComponents = extractDateTimeComponents(recordingName);
robot.keyToggle("shift", "down");
robot.keyTap("R");
robot.keyToggle("shift", "up");
robot.keyTap("e");
robot.keyTap("p");
robot.keyTap("l");
// ...
robot.keyTap(dateTimeComponents.year[0]);
robot.keyTap(dateTimeComponents.year[1]);
robot.keyTap(dateTimeComponents.year[2]);
robot.keyTap(dateTimeComponents.year[3]);
robot.keyTap(".");
robot.keyTap(dateTimeComponents.month[0]);
robot.keyTap(dateTimeComponents.month[1]);
robot.keyTap(".");
robot.keyTap(dateTimeComponents.day[0]);
// ...
robot.keyTap(dateTimeComponents.rest[5]);
robot.keyTap(".");
robot.keyTap("r");
robot.keyTap("c");
robot.keyTap("x");
}
function openTheRecording() {
console.log("...Opening the recording...");
const OPEN_RECORDING_BUTTON_LOCATION = [906, 753];
robot.moveMouse(
OPEN_RECORDING_BUTTON_LOCATION[0],
OPEN_RECORDING_BUTTON_LOCATION[1]
);
robot.mouseClick();
}
Έπειτα ανοίγει την εμφάνιση των παικτών που παίζουν και τραβάει μια φωτογραφία κάνοντας έπειτα περικοπή αυτό το σημείο της οθόνης:

Ας δούμε και τον αντίστοιχο κώδικα:
function openTheDropdownMenuWithUsernames() {
console.log("...Opening the dropdown...");
const DROPDOWN_MENU_LOCATION = [622, 908];
robot.moveMouse(DROPDOWN_MENU_LOCATION[0], DROPDOWN_MENU_LOCATION[1]);
robot.mouseClick();
}
async function takeScreenshotAndCropUsernamesPart() {
console.log("...Taking a screenshot and cropping usernames part...");
try {
await screenshot({ filename: "shot.jpg" });
await cropScreenshotTaken();
} catch (error) {
console.log("Error in taking screenshot", error);
}
}
async function cropScreenshotTaken() {
const SCREENSHOT_LOCATION = [622, 908];
const SCREENSHOT_SIZE = [180, 80];
try {
await sharp("shot.jpg")
.extract({
left: SCREENSHOT_LOCATION[0],
top: SCREENSHOT_LOCATION[1],
width: SCREENSHOT_SIZE[0],
height: SCREENSHOT_SIZE[1],
})
.toFile("shot2.jpg");
console.log("...Image cropped and saved successfully.");
} catch (error) {
console.error("Error in cropping screenshot taken:", error);
}
}
Έχοντας τραβήξει το παρακάτω στιγμιότυπο:

Μπορούμε να πάμε στο τελευταίο και σημαντικότερο σημείο της διαδικασίας, αυτή της αναγνώρισης των δύο αντιπάλων. Εδώ η βιβλιοθήκη Tesseract θα πάρει την εικόνα και θα αναγνωρίσει τα usernames των δύο παικτών:
async function tesseractRecognizeUsernamesInScreenshot() {
console.log(
"...Waiting for tesseract to recognize usernames in screenshot..."
);
try {
const tesseractResponse = await tesseract.recognize("shot2.jpg", "eng");
return tesseractResponse.data.text;
} catch (error) {
console.error("Error in tesseract usernames recognition:", error);
}
}
Στο τελευταίο βήμα, λαμβάνουμε την απάντηση της Tesseract, μετακινούμε την εγγραφή στο κατάλληλο σημείο (προς το παρόν δεν έχω αποφασίσει ποιό θα είναι αυτό) και ανοίγουμε εκ νέου το παράθυρο πληκτρολόγησης εγγραφής για να επαναλάβουμε τη διαδικασία.
function moveRecordingToFolderForThoseUserMatches(
recordingName,
usernamesRecognized
) {
// It gets text input like this 'Haki Terror\nSkelo\n' and returns this 'Haki Terror-Skelo'
const usernames = usernamesRecognized.split("\n");
const matchesFolderName = usernames[0] + "-" + usernames[1];
console.log(
`...Moving ${recordingName} to folder: ${matchesFolderName} 🟢 `
);
// TODO, archiving policy yet to be defined
}
function openWindowToWatchAnotherRecording() {
console.log("...Opening new window to watch another recording...\n");
const MENU_LOCATION = [1533, 138];
const RECORDED_GAMES_BUTTON_LOCATION = [895, 561];
robot.moveMouse(MENU_LOCATION[0], MENU_LOCATION[1]);
robot.mouseClick();
robot.moveMouse(
RECORDED_GAMES_BUTTON_LOCATION[0],
RECORDED_GAMES_BUTTON_LOCATION[1]
);
robot.mouseClick();
}
Ας δούμε λοιπόν το robot μας εν δράσει:
Και η ιδέα πέτυχε! Μπορώ πλέον να αφήσω το robot να δεί όλα τα παιχνίδια του Skelo και να βρεί αυτά εναντίον μου.
Από άποψη απόδοσης, έβαλα το χρόνο πληκτρολόγησης και μετακίνησης ποντικιού αρκέτα μεγάλα για τις ανάγκες του βίντεο:
robot.setKeyboardDelay(20);
robot.setMouseDelay(2000);
Όμως αυτό μπορεί να ρυθμιστεί για να γίνεται η προβολή των εγγραφών ακόμη πιο γρήγορα.
Τελικές σκέψεις - Μέρος 1
Ολοκληρώνοντας το άρθρο, και έτσι για το φιλοσοφικό της υπόθεσης, θα κλείσω με τη φράση 'πανταχού παρών αυτοματισμός', από το βιβλίο του Andrew Hunt και Thomas David - ο Πρακτικός Προγραμματιστής. Και όταν λέμε 'πανταχού' το εννοούμε, ακόμη και στα πιο σουρεάλ σενάρια. Prostagma εκτός από ένα θρυλικό meme, σημαίνει και να εκτελώ μια εντολή, είτε αυτή απευθύνεται στους χωρικούς του aom, είτε στα robot μας. Και αν και προφανώς δεν περιμέναμε αυτό το άρθρο για να αναγνωρίσουμε την αξία του αυτοματισμού, ας υποδεχτούμε την επικείμενη εποχή αυτοματισμού, robot ή όπως λέγεται και το άρθρο, an Age of Prostagma.
Ευχαριστώ για την ανάγνωση.
Επιπλέον λύση 1
Και εκεί που έλεγα ότι η μέρα μου δεν μπορούσε να πάει καλύτερα, ανακάλυψα και εγώ όλα τα 2500 δικά μου καταγεγραμμένα παιχνίδια. Μετά από λίγο ψάξιμο βρήκα ότι το Steam τα αποθηκεύει σε συγκεκριμένο σημείο στο δίσκο. Έχοντας λοιπόν και τα δικά μου παιχνίδια και του Skelo, αναρωτήθηκα αν υπήρχε πιο αποτελεσματικός τρόπος να βρεθούν τα παιχνίδια μας. Και υπήρχε!
Αναλύοντας το τελευταίο παιχνίδι μου ενάντια στο Skelo παρατήρησα κάτι κοινό. Και αυτό ήταν η ώρα τελευταία τροποποιήσης των αρχείων.

Και τα δύο αρχεία είχαν ακριβώς μια ώρα διαφορά, που είναι λογικό δεδομένου ότι με το Skelo ζούμε σε διαφωρετικές χώρες, άρα έχουμε και διαφορά ώρας. Έφτιαξα λοιπόν ένα bashοσκριπτο το οποίο ελέγχει και βρίσκει όσα αρχεία έχουν ακριβώς μια ώρα τελευταίας τροποποιήσης μεταξύ τους, ανάμεσα σε δύο φακέλους.
#!/bin/bash
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <directory1> <directory2>"
exit 1
fi
dir1="$1"
dir2="$2"
# Find all files in both directories and store them in arrays
mapfile -t files_dir1 < <(find "$dir1" -type f)
mapfile -t files_dir2 < <(find "$dir2" -type f)
# Iterate through each file in the first directory
for file1 in "${files_dir1[@]}"; do
echo $file1
# Iterate through each file in the second directory
for file2 in "${files_dir2[@]}"; do
# echo "Comparing times between \"$file1\" and \"$file2\""
# Get modification times in seconds since epoch
mtime1=$(stat -c %Y "$file1")
mtime2=$(stat -c %Y "$file2")
# Calculate the time difference in seconds
time_diff=$((mtime2 - mtime1))
abs_time_diff=${time_diff#-}
# echo "Time difference: $abs_time_diff seconds"
# Check if the time difference is exactly 1 hour (3600 seconds)
if [ "$abs_time_diff" -eq 3600 ]; then
echo $file1
echo $file2
echo "The modification times are exactly 1 hour apart."
fi
# else
# echo "The modification times are not exactly 1 hour apart."
# fi
done
done
Και, αν και το σκριπτάκι απέχει πολύ από το να είναι βέλτιστο, δούλεψε και μου επέστρεψε όλα τα παιχνιδιά μου εναντίον του Skelo:
./Desktop/savegame_haki_og/rcx/Replay v2.8 @2023.12.31 210150.rcx
./Desktop/savegame_skelo_og/rcx/Replay v2.8 @2023.12.31 200150.rcx
./Desktop/savegame_haki_og/rcx/Replay v2.8 @2023.12.02 011802.rcx
./Desktop/savegame_skelo_og/rcx/Replay v2.8 @2023.12.02 001801.rcx
./Desktop/savegame_haki_og/rcx/Replay v2.8 @2023.12.06 003856.rcx
./Desktop/savegame_skelo_og/rcx/Replay v2.8 @2023.12.05 233855.rcx
./Desktop/savegame_haki_og/rcx/Replay v2.8 @2023.12.05 225003.rcx
./Desktop/savegame_skelo_og/rcx/Replay v2.8 @2023.12.05 215003.rcx
./Desktop/savegame_haki_og/rcx/Replay v2.8 @2023.12.18 003154.rcx
./Desktop/savegame_skelo_og/rcx/Replay v2.8 @2023.12.17 233411.rcx
./Desktop/savegame_haki_og/rcx/Replay v2.8 @2023.07.13 171923.rcx
./Desktop/savegame_skelo_og/rcx/Replay v2.8 @2023.07.13 184351.rcx
./Desktop/savegame_haki_og/rcx/Replay v2.8 @2023.09.13 004102.rcx
./Desktop/savegame_skelo_og/rcx/Replay v2.8 @2023.09.12 234102.rcx
./Desktop/savegame_haki_og/rcx/Replay v2.8 @2023.09.13 010419.rcx
./Desktop/savegame_skelo_og/rcx/Replay v2.8 @2023.09.13 000419.rcx
./Desktop/savegame_haki_og/rcx/Replay v2.8 @2023.09.13 005359.rcx
./Desktop/savegame_skelo_og/rcx/Replay v2.8 @2023.09.12 235359.rcx
./Desktop/savegame_haki_og/rcx/Replay v2.8 @2023.10.04 002937.rcx
./Desktop/savegame_skelo_og/rcx/Replay v2.8 @2023.10.03 232937.rcx
Επιπλέον λύση 2
Εν τέλει, υπήρχε τρόπος να έχεις πρόσβαση στα μεταδεδομένα του rcx αρχείου. Αφού μοιράστηκα την εμπειρία μου με το φίλο και συν-συνγραφέα του Πυρσού, Σπυράκου, μου πρότεινε δύο repositories που θα έλεγε κάποιος ότι θα μου φαινόντουσαν χρήσιμα:
Και τα δύο repo ξέραν να αποσυμπιέζουν τα περιεχόμενα των RCX αρχείων και να βρίσκουν σε ποιές θέσεις του αρχείου υπήρχε η οφέλιμη πληροφορία. Δεν ξέρω πως και τι έκαναν και κατάφεραν να βρούν το τρόπο να βροόυν τη πληροφορία αυτή για τα συγκεκριμένα είδη αρχείου, αν το έκαναν reverse engineer ή αν είναι βρήκαν κάτι μπροστά στα μάτια μου που δεν είδα ποτέ, αλλά όπως και να έχει respect. Βασιζόμενος στη δουλειά τους, έγραψα και εγώ με τη σειρά μου ένα Python σκριπτάκι που έπαιρνε σαν είσοδο το φάκελο των καταγραφών, διάβαζε τα περιεχόμενα της συγκεκριμένης καταγραφής και μπορούσε να φιλτράρει παιχνίδια απέναντι σε συγκεκριμένους αντιπάλους (ανοίγωντας το δρόμο και για άλλα είδη αναζήτησης).
import zlib
import struct
import argparse
import xml.etree.ElementTree as ET
import os
class RecordingsFacade:
def __init__(self, filepath, is_ee):
self.filepath = filepath
with open(filepath, "rb") as f:
all = f.read()
# Check the header magic
magic = all[:4]
if magic != b"l33t":
raise ValueError("Bad magic value")
size = struct.unpack("<I", all[4:8])[0]
rest = all[8:]
decomper = zlib.decompressobj()
# Decompress data
try:
decomp = decomper.decompress(rest)
except zlib.error as e:
raise ValueError("Recording corrupt")
# Sanity check size
if size != len(decomp):
raise ValueError("Error in decompression. File might be corrupted")
self.decomp = decomp
self.seek = 1474 if is_ee else 1466
self.gameSettingsXml = self.read_file()
decodedXML = self.gameSettingsXml.decode("utf-16")
#print("Decoding game..." + filepath)
#print(decodedXML)
def is_game_against(self, opponent, expectedNoPlayers):
root = ET.fromstring(self.gameSettingsXml)
players = list(map(lambda x: x.find("Name").text, root.findall("Player")))
noPlayers = root.find("NumPlayers").text
if(self.any_equals(players, opponent) and noPlayers == str(expectedNoPlayers)):
print(players[0] + "-" + players[1] + ": " + self.filepath)
def any_equals(self, array, string):
return any(item == string for item in array)
def read_section(self, totalSize):
read = b""
while totalSize > 0:
blockSize = self.read_four()
if blockSize == 0:
raise ValueError("Zero block size")
toRead = min(totalSize, blockSize)
read += self.read_n(toRead)
totalSize -= toRead
return read
def read_file(self):
totalSize = self.read_four()
return self.read_section(totalSize)
def read_four(self):
data = struct.unpack("<I", self.decomp[self.seek:self.seek+4])[0]
self.seek += 4
return data
def read_one(self):
data = struct.unpack("B", self.decomp[self.seek:self.seek+1])[0]
self.seek += 1
return data
def read_two(self):
data = struct.unpack("H", self.decomp[self.seek:self.seek+2])[0]
self.seek += 2
return data
def read_n(self,n):
data = self.decomp[self.seek:self.seek+n]
self.seek += n
return data
def main():
parser = argparse.ArgumentParser()
parser.add_argument('dirpath')
args = parser.parse_args()
for filename in os.listdir(args.dirpath):
filepath = os.path.join(args.dirpath, filename)
try:
if os.path.isfile(filepath) and filename.endswith(".rcx"):
RecordingsFacade(filepath, is_ee=True).is_game_against("Skelo", 2)
except Exception as e:
e
#print(e)
#print("Something went wrong...Possibly a corrupted recording")
# Do nothing
if __name__ == "__main__":
main()
Αν παρατηρήσουμε το τρόπο με τον οποίο γίνεται η ανάκτηση της πληροφορίας για τη καταγραφή, φαίνεται μια απλή διαδικασία αποσυμπίεσης. Το πως βρήκαν όμως ότι το magic value του file header είναι εμπνευσμένο από το l33t culture παραμένει ένα μυστήριο για μένα.
def __init__(self, filepath, is_ee):
self.filepath = filepath
with open(filepath, "rb") as f:
all = f.read()
# Check the header magic
magic = all[:4]
if magic != b"l33t":
raise ValueError("Bad magic value")
size = struct.unpack("<I", all[4:8])[0]
rest = all[8:]
decomper = zlib.decompressobj()
# Decompress data
try:
decomp = decomper.decompress(rest)
except zlib.error as e:
raise ValueError("Recording corrupt")
# Sanity check size
if size != len(decomp):
raise ValueError("Error in decompression. File might be corrupted")
self.decomp = decomp
self.seek = 1474 if is_ee else 1466
self.gameSettingsXml = self.read_file()
decodedXML = self.gameSettingsXml.decode("utf-16")
print("Decoding game..." + filepath)
print(decodedXML)
Η παραπάνω μέθοδος θα μας δώσει τη παρακάτω πληροφορία για τη καταγραφή Replay v2.8 @2023.12.31 210150.rcx
:
<GameSettings>
<GameType>2</GameType>
<Filename>Anatolia</Filename>
<CurrentPlayer>1</CurrentPlayer>
<ScenarioFilename></ScenarioFilename>
<FilenameCRC>0</FilenameCRC>
<GameStartTime>1704049304</GameStartTime>
<MapVisibility>0</MapVisibility>
<WorldResources>1</WorldResources>
<MapSize>0</MapSize>
<RestrictPauses>5</RestrictPauses>
<GameFlags LockedTeams="" />
<TitanMode>0</TitanMode>
<TreatyLength>0</TreatyLength>
<DayNightCycle>0</DayNightCycle>
<HourOfAlpaca>0</HourOfAlpaca>
<GameMode>0</GameMode>
<HandicapMode>0</HandicapMode>
<MapSeed>9747</MapSeed>
<Difficulty>0</Difficulty>
<NumPlayers>2</NumPlayers>
<Player ClientIndex="0" ControlledPlayer="1" >
<Name>Haki Terror</Name>
<Rating>1925.000000</Rating>
<Type>0</Type>
<TransformColor1>0</TransformColor1>
<TransformColor2>0</TransformColor2>
<Team>255</Team>
<Civilization>7</Civilization>
<AIPersonality></AIPersonality>
</Player>
<Player ClientIndex="1" ControlledPlayer="2" >
<Name>Skelo</Name>
<Rating>1824.000000</Rating>
<Type>0</Type>
<TransformColor1>0</TransformColor1>
<TransformColor2>0</TransformColor2>
<Team>255</Team>
<Civilization>4</Civilization>
<AIPersonality></AIPersonality>
</Player>
</GameSettings>
Το οποίο είναι παραπάνω από αρκετό για να βρώ τα παιχνίδια μου.
Οπότε τρέχοντας το παρακάτω script:
$ python3 extract_recording_info.py ~/Downloads/savegame
Θα πάρω τα παιχνίδια μου ενάντια του Skelo (και μάλιστα παραπάνω από τις παραπάνω μεθόδους):

Δεν ξέρω αν τυφλώθηκα από τον ενθουσιασμό μου να γράψω ένα ρομπότ που θα παρακολουθούσε παιχνίδια αντί για εμένα, αλλά το γεγονός ότι υποτίμησα των ύπαρξη άλλων προγραμματιστών που είχαν την ίδια ανάγκη με εμένα (ακόμα και αν αυτή ήταν φαινομενικά μικρή και άκυρη), ήταν λάθος. Οπότε, αν και κοινή λογική, πάντα να εξαντλείτε τα περιθώρια αναζήτησης πριν προχωρήσετε στις χακιές σας.
Τελικές σκέψεις - Μέρος 2
Έχοντας προσεγγίσει το πρόβλημα με 3 εντελώς διαφορετικούς τρόπους, αποφάσισα να γράψω ένα 2ο μέρος τελικών σκέψεων. Αν και ξεκίνησα τελείως ανορθόδοξα, η όλη διαδικασία τελείως ετερογενών λύσεων θεωρώ ότι συνοψίζει την όλη φιλοσοφία και το λόγο που ασχολούμαστε με τα κομπιούτερ, που λέει και η γιαγιά μου. Το να σκεφτόμαστε με ποιά χακιά θα λύσουμε ένα πρόβλημα, να την δοκιμάζουμε και εν τέλει να παίζει. Και ειδικά όταν αυτό γίνεται και χωρίς να πολυσκεφτόμαστε και το τεχνικό χρέος, το κάνει ακόμα πιο καλό. Αυτά.
Τέλος
Έχοντας πει πολλά, μπορείτε τώρα να απολαύσετε τα παιχνίδια μας: