Last active
September 17, 2017 12:10
-
-
Save cb109/8533611ffc05dbeab43a5d2ec97fe6d5 to your computer and use it in GitHub Desktop.
The Patrician II: OCR based Trade Recommendations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
_________________________________________________ | |
___ __ _ _ ________ | |
/ _ \___ _/ /_____(_)___(_)__ ____ / _/ _/ | |
/ ___/ _ `/ __/ __/ / __/ / _ `/ _ \ _/ /_/ / | |
/_/ \_,_/\__/_/ /_/\__/_/\_,_/_//_/ /___/___/ | |
_________________________________________________ | |
You should buy in this town: | |
beer for max. 52 | |
You should sell in this town: | |
cloth for min. 400 | |
fish for min. 650 | |
honey for min. 200 | |
iron goods for min. 500 | |
spices for min. 500 | |
timber for min. 60 | |
whale oil for min. 160 | |
wine for min. 400 | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
""" | |
Give live trading recommendations while playing The Patrician II. | |
The current 'Trading Goods' window's text is analyzed (OCR done via a | |
3rd party tool) and checked against a price table. Recommended buy/sell | |
actions are printed to the console. | |
Requirements: | |
OS: | |
- Windows | |
Software: | |
- Patrician II (english version) | |
- Capture2Text_CLI.exe | |
Download it from: http://capture2text.sourceforge.net | |
and make sure to add its directory to the PATH variable. | |
Usage: | |
- Start The Patrician II, load a game and while in a city, open the | |
'Trading Goods' window by clicking on a trade port building. Make | |
sure to not to overlap the window with your mouse, keep it | |
outside. | |
- Open a console and move it to a second screen. | |
- Run the script like: | |
$ python patrician2_trade_recommendations.py | |
The console should show trade recommendations. | |
""" | |
import os | |
import re | |
import subprocess | |
import time | |
HEADER = """ | |
_________________________________________________ | |
___ __ _ _ ________ | |
/ _ \___ _/ /_____(_)___(_)__ ____ / _/ _/ | |
/ ___/ _ `/ __/ __/ / __/ / _ `/ _ \ _/ /_/ / | |
/_/ \_,_/\__/_/ /_/\__/_/\_,_/_//_/ /___/___/ | |
_________________________________________________ | |
""" | |
DEBUG = False | |
WAIT_IN_BETWEEN_SECONDS = 2 | |
# Note: Assuming a screen resolution of 1280x720. | |
# Format is: X1 Y1 X2 Y2 | |
TRADING_GOODS_SCREEN_RECT = "311 331 569 758" | |
# Taken from: http://www.spieletipps.de/tipps-10027-patrizier-2-preisliste/ | |
# good: [buy-price, sell-price] | |
TRADING_RANGES = { | |
"beer": [39, 56], | |
"tiles": [88, 110], | |
"cloth": [229, 286], | |
"fish": [479, 599], | |
"grain": [119, 149], | |
"hemp": [453, 566], | |
"honey": [121, 151], | |
"iron goods": [285, 356], | |
"leather": [241, 302], | |
"meat": [1041, 1302], | |
"iron ore": [953, 1191], | |
"pitch": [61, 76], | |
"pottery": [182, 227], | |
"salt": [31, 39], | |
"skins": [748, 935], | |
"spices": [296, 370], | |
"timber": [61, 76], | |
"whale oil": [91, 114], | |
"wine": [244, 305], | |
"wool": [928, 1160], | |
} | |
# For goods consisting of two words like 'iron goods' OCR sometimes does | |
# not recognize the separating space, so add aliases for this and other | |
# common mistakes. | |
for key, value in TRADING_RANGES.items(): | |
if " " in key: | |
alias = key.replace(" ", "") | |
TRADING_RANGES[alias] = value | |
TRADING_RANGES["moat"] = TRADING_RANGES["meat"] | |
TRADING_RANGES["whalc oil"] = TRADING_RANGES["whale oil"] | |
TRADING_RANGES["whalcoil"] = TRADING_RANGES["whaleoil"] | |
pattern_digit = re.compile("\d+") | |
pattern_digit_only = re.compile("^\d+$") | |
pattern_good = re.compile("(?P<good>^[a-z ]+[^ \d][a-z])(?P<pricerange>.*)") | |
def get_text_from_trading_goods_screen(): | |
args = [ | |
"Capture2Text_CLI", | |
"-l", "English", | |
"--screen-rect", TRADING_GOODS_SCREEN_RECT, | |
"--line-breaks", | |
] | |
text = subprocess.check_output(args) | |
return text | |
def should_trade(mode, good, price, TRADING_RANGES=TRADING_RANGES): | |
"""Return price limit if we should trade, False otherwise.""" | |
good_buy, good_sell = TRADING_RANGES[good] | |
if mode == "buy": | |
return good_buy if price <= good_buy else False | |
elif mode == "sell": | |
return good_sell if price >= good_sell else False | |
def compensate_common_mistakes(token): | |
# Round boxes around prices are often recognized as a "J" or "')". | |
removals = ["J", "'", ")", ",", "\\", "r"] | |
replacements = [ | |
("o", "0"), | |
("O", "0"), | |
("s", "5"), | |
("s", "5"), | |
] | |
for removal in removals: | |
token = token.replace(removal, "") | |
for this, that in replacements: | |
token = token.replace(this, that) | |
token = token.strip() | |
return token | |
def analyze_text_for_recommandations(text): | |
recommendations = [] | |
lines = text.split("\n") | |
for line in lines: | |
try: | |
digits = pattern_digit.findall(line) | |
if not digits: | |
continue | |
match = pattern_good.match(line) | |
if not match: | |
continue | |
good = match.group("good") | |
pricerange = match.group("pricerange") | |
tokens = pricerange.split(" ") | |
filtered_tokens = [] | |
for token in tokens: | |
token = compensate_common_mistakes(token) | |
if not pattern_digit_only.match(token): | |
continue | |
filtered_tokens.append(token) | |
if DEBUG: | |
print(good, "tokens:", tokens) | |
print(good, "filtered_tokens:", filtered_tokens) | |
amount = buy_for = sell_for = None | |
in_stock = len(filtered_tokens) == 3 | |
if in_stock: | |
amount, buy_for, sell_for = [int(t) for t in filtered_tokens] | |
else: | |
_, sell_for = [int(t) for t in filtered_tokens] | |
can_buy = amount is not None and buy_for is not None | |
can_sell = sell_for is not None | |
if can_buy: | |
price = should_trade("buy", good, buy_for) | |
if price: | |
recommendations.append(("buy", good, price)) | |
if can_sell: | |
price = should_trade("sell", good, sell_for) | |
if price: | |
recommendations.append(("sell", good, price)) | |
except (KeyError, ValueError) as err: | |
if DEBUG: | |
print(err) | |
return recommendations | |
def format_good(good, digits=12): | |
good = str(good) | |
add = digits - len(good) | |
return good + (" " * add) | |
def format_price(price, digits=4): | |
price = str(price) | |
add = digits - len(price) | |
return (" " * add) + price | |
def clear_screen(): | |
os.system('cls') | |
def print_report(recommendations, clear=True): | |
to_buy = [rec for rec in recommendations if rec[0] == "buy"] | |
to_sell = [rec for rec in recommendations if rec[0] == "sell"] | |
if clear: | |
clear_screen() | |
print(HEADER) | |
if to_buy: | |
print("You should buy in this town:\n") | |
for _, good, price in to_buy: | |
print(" {} for max. {}".format(format_good(good), | |
format_price(price))) | |
print("") | |
if to_sell: | |
print("You should sell in this town:\n") | |
for _, good, price in to_sell: | |
print(" {} for min. {}".format(format_good(good), | |
format_price(price))) | |
if not recommendations: | |
print("Nothing to analyze. Please open a 'Trading Goods' window.") | |
print("") | |
def main(): | |
time.sleep(WAIT_IN_BETWEEN_SECONDS) | |
output = get_text_from_trading_goods_screen() | |
recommendations = analyze_text_for_recommandations(output) | |
if not DEBUG: | |
print_report(recommendations) | |
while True: | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment