Created
May 28, 2015 18:20
-
-
Save terrbear/19731d95d0bc6481d9fd to your computer and use it in GitHub Desktop.
This file contains hidden or 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
#tunables! | |
VERBOSE = false | |
MAX_DEBT = 15_000 | |
MIN_DEBT = 500 | |
MAX_APR = 30 | |
DEBT_MONEY = 1500 | |
SIMULATION_ATTEMPTS = 1000 | |
class Debt | |
attr_accessor :name, :amount, :apr, :min_payment | |
def initialize(name, amount, apr, min_payment) | |
self.name = name | |
self.amount = amount | |
self.apr = apr | |
self.min_payment = min_payment | |
end | |
def pay!(amount) | |
self.amount -= amount | |
if self.amount < 0 | |
overpaid = self.amount.abs | |
self.amount = 0 | |
return overpaid | |
end | |
return 0 | |
end | |
def to_s | |
self.name.to_s | |
end | |
def compound! | |
return if self.paid? | |
self.amount += self.interest | |
end | |
def interest | |
interest = self.amount | |
daily_rate = self.apr.to_f / (365 * 100) | |
30.times do | |
interest += (daily_rate * interest) | |
end | |
interest - self.amount | |
end | |
def to_t | |
[self.name.capitalize.rjust(10), | |
self.amount.to_i.to_s.rjust(10), | |
self.apr.to_i.to_s.rjust(10), | |
self.min_payment.to_i.to_s.rjust(10) | |
].join("|") | |
end | |
def paid? | |
self.amount <= 0 | |
end | |
def dup | |
Debt.new(name, amount, apr, min_payment) | |
end | |
end | |
class Wallet | |
attr_accessor :debts | |
def initialize(strategy) | |
@strategy = strategy | |
self.debts = [] | |
end | |
def dup | |
Wallet.new(@strategy).tap do |w| | |
w.debts = self.debts.map{|d| d.dup} | |
end | |
end | |
def unpaid_debts | |
debts.reject{|d| d.paid?} | |
end | |
def pay_min_payments(leftover_debts, amount) | |
return amount if leftover_debts.empty? | |
debt = nil | |
Strategy.without_memory{debt = @strategy.choose(leftover_debts, amount)} | |
next_set = leftover_debts - [debt] | |
if amount > debt.min_payment | |
amount += debt.pay!(debt.min_payment) | |
amount -= debt.min_payment | |
else | |
puts "Warning, can't pay min payment to #{debt}" if VERBOSE | |
end | |
return pay_min_payments(next_set, amount) | |
end | |
def pay!(amount) | |
debts.each{|d| d.compound!} | |
amount = pay_min_payments(unpaid_debts, amount) | |
while amount > 0 | |
debt = @strategy.choose(debts, amount) | |
puts "paying #{amount.to_i} to #{debt}" if VERBOSE | |
break if debt.nil? | |
amount = debt.pay!(amount) | |
end | |
end | |
def to_s | |
([["Name".center(10), | |
"Amount".center(10), | |
"APR".center(10), | |
"Min Payment".center(10)].join("|")] + | |
self.debts.map{|d| d.to_t}).join("\n") | |
end | |
def empty? | |
self.debts.select{|d| d.amount > 0}.empty? | |
end | |
end | |
class Strategy | |
class << self | |
@memory = true | |
def memory | |
@memory | |
end | |
def without_memory(&block) | |
@memory = false | |
yield | |
@memory = true | |
end | |
end | |
end | |
class SmallestAmount < Strategy | |
def choose(debts, payment) | |
debts.sort{|a,b| a.amount <=> b.amount}.reject{|d| d.paid?}.first | |
end | |
end | |
class HighestInterest < Strategy | |
def choose(debts, payment) | |
debts.sort{|a,b| b.apr <=> a.apr}.reject{|d| d.paid?}.first | |
end | |
end | |
class LowestInterest < Strategy | |
def choose(debts, payment) | |
debts.sort{|a,b| a.apr <=> b.apr}.reject{|d| d.paid?}.first | |
end | |
end | |
class WhackAMole < Strategy | |
def choose(debts, payment) | |
debts.sort{|a, b| b.amount <=> a.amount}.reject{|d| d.paid?}.first | |
end | |
end | |
class HighestCard < Strategy | |
@highest = nil | |
def choose(debts, payment) | |
if Strategy.memory | |
if @highest.nil? || @highest.paid? | |
@highest = choose_without_memory(debts) | |
end | |
return @highest | |
else | |
return choose_without_memory(debts) | |
end | |
end | |
def choose_without_memory(debts) | |
debts.sort{|a, b| b.amount <=> a.amount}.reject{|d| d.paid?}.first | |
end | |
end | |
class Sprinter < HighestInterest | |
def months | |
3 | |
end | |
def choose(debts, payment) | |
debts.select{|d| d.amount < (payment * self.months) && !d.paid?}.sort{|a, b| a.amount <=> b.amount}.first || super(debts, payment) | |
end | |
end | |
class Sprinter6 < Sprinter | |
def months | |
6 | |
end | |
end | |
class Sprinter12 < Sprinter | |
def months | |
12 | |
end | |
end | |
class Sprinter1 < Sprinter | |
def months | |
1 | |
end | |
end | |
def new_wallet(strategy) | |
wallet = Wallet.new(strategy) | |
wallet.debts = $debts | |
wallet.dup | |
end | |
def payoff(wallet) | |
count = 0 | |
loop do | |
break if wallet.empty? | |
wallet.pay!(DEBT_MONEY) | |
count += 1 | |
if count % 10 == 0 && VERBOSE | |
banner " After #{count} " | |
puts wallet | |
end | |
if count > 100_000 | |
puts "Never gonna finish" | |
exit 1 | |
end | |
end | |
return count | |
end | |
def test_strategy(strategy) | |
payoff new_wallet(strategy) | |
end | |
def banner(str = '') | |
puts "*" * 80 | |
puts str.upcase.center(80, "*") | |
puts "*" * 80 | |
end | |
def create_debts! | |
$debts = [] | |
count = rand(7) + 2 | |
count.times do |i| | |
balance = rand(MAX_DEBT) + MIN_DEBT | |
apr = rand(MAX_APR) | |
min_payment = balance * 0.01 | |
$debts << Debt.new("Debt#{i}", balance, apr, min_payment) | |
end | |
end | |
scoreboard = { | |
"Smallest Amount" => {wins: 0, losses: 0}, | |
"Highest Interest" => {wins: 0, losses: 0}, | |
"Lowest Interest" => {wins: 0, losses: 0}, | |
"Whack A Mole" => {wins: 0, losses: 0}, | |
"Highest Card" => {wins: 0, losses: 0}, | |
"Sprinter" => {wins: 0, losses: 0}, | |
"Sprinter 6" => {wins: 0, losses: 0}, | |
"Sprinter 12" => {wins: 0, losses: 0}, | |
"Sprinter 1" => {wins: 0, losses: 0}, | |
} | |
SIMULATION_ATTEMPTS.times do |time| | |
begin | |
create_debts! | |
strategies = {} | |
strategies["Smallest Amount"] = test_strategy SmallestAmount.new | |
strategies["Highest Interest"] = test_strategy HighestInterest.new | |
strategies["Lowest Interest"] = test_strategy LowestInterest.new | |
strategies["Whack A Mole"] = test_strategy WhackAMole.new | |
strategies["Highest Card"] = test_strategy HighestCard.new | |
strategies["Sprinter"] = test_strategy Sprinter.new | |
strategies["Sprinter 12"] = test_strategy Sprinter12.new | |
strategies["Sprinter 6"] = test_strategy Sprinter6.new | |
strategies["Sprinter 1"] = test_strategy Sprinter1.new | |
sorted = strategies.sort{|a, b| a[1] <=> b[1]} | |
winner = sorted.first[1] | |
loser = sorted.last[1] | |
sorted.select{|strat| strat[1] == winner}.each{|winners| scoreboard[winners.first][:wins] += 1} | |
if winner != loser | |
sorted.select{|strat| strat[1] != winner}.each{|losers| scoreboard[losers.first][:losses] += 1} | |
end | |
rescue | |
next | |
end | |
end | |
banner " scoreboard " | |
puts [ | |
"Name".center(20), | |
"Wins".center(10), | |
"Losses".center(10), | |
"Pct".center(5) | |
].join(" | ") | |
out = scoreboard.keys.map do |k| | |
vals = scoreboard[k] | |
[k.center(20), | |
vals[:wins].to_s.rjust(10), | |
vals[:losses].to_s.rjust(10), | |
("%.2f" % (vals[:wins].to_f / (vals[:wins] + vals[:losses]))).rjust(5)].join(" | ") | |
end.join("\n") | |
puts out |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment