#!/usr/local/bin/ruby -w

require 'test/unit'
require '../src/nim'

# To Do: Rename most or all tests to include the word "Should"

class TC_NimGame < Test::Unit::TestCase
    def setup
        @p1 = Player.new(FixedMoveStrategy.new(1), AlwaysBidStrategy.new)
        @p2 = Player.new(FixedMoveStrategy.new(2), AlwaysBidStrategy.new)

        @theGame = NimGame.new @p1, @p2
    end

    def test_startGame
        assert_equal DefaultPileSize, @theGame.pieces
    end

    def test_clone
    	clone = @theGame.clone
    	
    	assert_equal clone, @theGame
    	clone.askPlayerToMove
    	
    	assert_not_equal clone, @theGame
    	
    	assert_same @p1, @theGame.players[0]
    	assert_same @p2, @theGame.players[1]
    	
        p3 = Player.new(FixedMoveStrategy.new(1), AlwaysBidStrategy.new)

    	clone.exchangePlayer(p3, 0)

    	assert_same @p1, @theGame.players[0]
    	assert_same @p2, @theGame.players[1]
    end

    def test_cloneShouldProduceDistinctStrings
		@theGame.debugInfo << "Test1"

		clone = @theGame.clone
		
		clone.debugInfo << "Test2"
		
		assert_equal "Test1", @theGame.debugInfo

		assert_equal "Test1Test2", clone.debugInfo
    end

    def test_applyMove!
        move = NimMove.new(1)

        @theGame.applyMove! move

        assert_equal 9, @theGame.pieces

        8.times { @theGame.applyMove! move }

        assert_equal 1, @theGame.pieces

        assert_raises(StandardError) { ||
            move = NimMove.new(2)
            @theGame.applyMove! move
        }
    end

    def test_moveIsValid?
        assert @theGame.moveIsValid?(NimMove.new(1))
        assert @theGame.moveIsValid?(NimMove.new(2))

        8.times { @theGame.applyMove! NimMove.new(1) }

        assert @theGame.moveIsValid?(NimMove.new(2))

        @theGame.applyMove! NimMove.new(1)

        assert   @theGame.moveIsValid?(NimMove.new(1))
        assert ! @theGame.moveIsValid?(NimMove.new(2))

        @theGame.applyMove! NimMove.new(1)
        assert ! @theGame.moveIsValid?(NimMove.new(1))
        assert ! @theGame.moveIsValid?(NimMove.new(2))
    end

    def test_isOver?
        assert ! @theGame.isOver?

        5.times { @theGame.applyMove! NimMove.new(2) }

        assert @theGame.isOver?
    end
end

class TC_NimMove < Test::Unit::TestCase
    def test_createMove
        move = NimMove.new(1)
        assert_equal 1, move.pieces

        move = NimMove.new(2)
        assert_equal 2, move.pieces

        assert_raises(StandardError) { ||
            move = NimMove.new(3)
        }
    end

    def test_eql
        assert_equal NimMove.new(1), NimMove.new(1)
        assert_not_equal NimMove.new(1), NimMove.new(2)
    end
end

class TC_FixedMoveStrategy < Test::Unit::TestCase
    def setup
        p1 = Player.new(FixedMoveStrategy.new(1), AlwaysBidStrategy.new)
        p2 = Player.new(FixedMoveStrategy.new(2), AlwaysBidStrategy.new)

        @nimGame = NimGame.new p1, p2
    end

    def test_getMove
        s = FixedMoveStrategy.new(1)
        assert_equal NimMove.new(1), s.getMove(@nimGame)

        s = FixedMoveStrategy.new(2)
        assert_equal NimMove.new(2), s.getMove(@nimGame)
        @nimGame.pieces = 1
        assert_equal NimMove.new(1), s.getMove(@nimGame)
    end

    def test_to_s
        s = FixedMoveStrategy.new(1)

        assert_equal("1\t", s.to_s)
    end
end

class TC_BiddingStrategy < Test::Unit::TestCase
    def setup
        p1 = Player.new(FixedMoveStrategy.new(1), AlwaysBidStrategy.new)
        p2 = Player.new(FixedMoveStrategy.new(2), AlwaysBidStrategy.new)

        @nimGame = NimGame.new p1, p2
    end

    def test_AlwaysBidStrategy
        s = AlwaysBidStrategy.new

        assert_equal 0.5, s.getBid(@nimGame)
    end

    def test_NeverBidStrategy
        s = NeverBidStrategy.new

        assert_nil s.getBid(@nimGame)
    end
end

class TC_Player < Test::Unit::TestCase
    def setup
        @p1 = Player.new(FixedMoveStrategy.new(1), AlwaysBidStrategy.new)
        @p2 = Player.new(FixedMoveStrategy.new(2), AlwaysBidStrategy.new)

        @nimGame = NimGame.new @p1, @p2
    end

    def test_makeYourMove
        assert_equal 10, @nimGame.pieces
        @nimGame.askPlayerToMove
        assert_equal 9, @nimGame.pieces
        @nimGame.askPlayerToMove
        assert_equal 7, @nimGame.pieces

        4.times { @nimGame.askPlayerToMove }

        assert ! @nimGame.isOver?

        @nimGame.askPlayerToMove

        assert @nimGame.isOver?

        assert_equal(-1 , @p1.funds)
        assert_equal( 1 , @p2.funds)
    end

    def test_bid
        assert_nil @p1.bid(@nimGame)

        @p1.funds = 10
        assert_equal 0.5, @p1.bid(@nimGame)

        @p1.funds = 0.5
        assert_equal 0.5, @p1.bid(@nimGame)

        @p1.funds = 0
        assert_nil @p1.bid(@nimGame)
    end

    def test_to_s
        s = @p1.to_s

        # Look for his fund balance, which is zero
        # because we didn't give him any.
        assert_match(/0.00/, s)
    end
end

class TC_Shill < Test::Unit::TestCase
    def test_to_s
    	s = Shill.new.to_s
    	
        assert_match(/<Shill:/, s)
        assert_match(/0.00/, s)
	end
end

class TC_Auctioneer < Test::Unit::TestCase
    def setup
        @alwaysBid = AlwaysBidStrategy.new
        @neverBid  = NeverBidStrategy.new

        @p1 = Shill.new
        @p2 = Shill.new

        @p3 = Player.new(FixedMoveStrategy.new(1), @alwaysBid, 10)
        @p4 = Player.new(FixedMoveStrategy.new(2), @alwaysBid, 10)
        @p5 = Player.new(FixedMoveStrategy.new(1), @neverBid)

        @nimGame = NimGame.new @p1, @p2

        @auctioneer = Auctioneer.new
    end

    def test_conductAuctionShouldAwardBidder
        [@p3, @p4, @p5].each { |p| @auctioneer.registerPlayer p }

        winningBidders, winningBid = @auctioneer.conductAuction @nimGame

        assert_same  @p3, winningBidders[0]
        assert_equal 0.5, winningBid
    end

    def test_conductAuctionShouldReturnEmptyListIfNoBidders
        @auctioneer.registerPlayer @p5

        winningBidders, winningBid = @auctioneer.conductAuction @nimGame

        assert winningBidders.empty?
        assert_nil winningBid
    end
end

class TC_GameHandler < Test::Unit::TestCase
    def setup
        @alwaysBid = AlwaysBidStrategy.new
        @neverBid  = NeverBidStrategy.new

        @p1 = Shill.new
        @p2 = Shill.new

        @p3 = Player.new(FixedMoveStrategy.new(1), @alwaysBid, 10)

        @nimGame = NimGame.new @p1, @p2

		@auctioneer = Auctioneer.new
        @handler = GameHandler.new @auctioneer
    end

    def test_doOneMove
        @auctioneer.registerPlayer @p3

        assert_equal   0,  @p1.funds
        assert_equal  10,  @p3.funds
        assert_same  @p1, @nimGame.nextPlayerToMove

		@handler.games.push @nimGame
        @handler.doOneMove

        assert_equal(0.5, @p1.funds)
        assert_equal(9.5, @p3.funds)
        assert_same  @p2, @handler.games.pop.nextPlayerToMove
    end
end

def perfectMove(position)
    case position % 3
        when 0 then 2
        when 1 then nil
        when 2 then 1
    end
end

def countCorrectMoves gameShell
    # A perfect score is gameShell.initialPileSize -- 
    # a correct move in every position.

    count = 0

    for stones in 1..gameShell.initialPileSize
        game = NimGame.new nil, nil, stones
        winningBidders, winningBid =
            gameShell.auctioneer.conductAuction game

        if winningBidders.empty?
            count += 1 if perfectMove(stones).nil?
        else
            piecesToTake = winningBidders[0].moveStrategy.piecesToTake
            count += 1 if piecesToTake == perfectMove(stones)
        end
    end

    count
end

class TC_CommandLineHandler < Test::Unit::TestCase

	def test_executeCommandLine
		knowns = [
			["-h",					/^Usage:/],
			["-e",					/^Terminated after 12 rounds/],
			["-e",					/^Position:  1 2 3 4 5 6 7 8 9 10/],
			["-e",					/Move:  x 1 2 x 1 2 x 1 2 x/],
			["-e -t awardToAll",	/^Terminated after 5 rounds/],
			["-f",					/^Player ID       \tAmount\tPos\tPieces\tFunds/],
			["-m",					/^x$/],
			["-m -p 9",				/^2$/],
			["-t byLot -s 1 -e",	/Move:  x 1 2 x 1 2 x 1 2 x/],
			["-t byLot -s 5 -e",	/Move:  x 1 2 x 1 1 x 2 2 x/],
			["-e -n 3",				/Move:  x 1 2 1 1 1 1 1 1 1/],
            ["-d",                  /Pieces\tBid\tPlayer ID       \tAmount\tPos\tPieces\tFunds\n\n/],
		]
		
		knowns.each { |line, result|
			s = CommandLineHandler.executeCommandLine line.split
			assert_match(result, s)
		}
	end
    
end

class TC_GameShell < Test::Unit::TestCase
    def setup
        @gameShell = GameShell.new
    end

    def test_UntrainedGameShouldMakeMistakes
        assert_equal 3, countCorrectMoves(@gameShell)
    end

    def test_TrainedGameShouldPlayPerfectly
		for pieces in [1, 9, 10, 11]
			gameShell = GameShell.new FirstInListTieBreakStrategy.new, pieces
			gameShell.executeManyGames
	
			assert_equal pieces, countCorrectMoves(gameShell)
        end
    end

    def test_LoopShouldTerminateAfterLearningCeases
		for pieces in 1..20
			gameShell = GameShell.new AwardTiesToAllBiddersStrategy.new, pieces
			roundCount, didBreak = gameShell.executeManyGames
	
			assert didBreak, "Did not break for pile size = #{pieces}"
        end
	end

    def test_executeManyGames
        @gameShell.executeManyGames

        s = @gameShell.showPlayerResources

        assert_match(/0.6\t 1\t2/, s)
        assert_match(/0.6\t 2\t2/, s)
        assert_match(/0.6\t 3\t2/, s)
    end

    def test_PlayerResourcesShouldNetToZero
    	totalFunds = 0.0

    	@gameShell.eachPlayer { | player |
    		totalFunds += player.funds
    	}
    	
    	assert_equal 20.0, totalFunds
    	
        @gameShell.executeManyGames

    	totalFunds = 0.0

    	@gameShell.eachPlayer { | player |
    		totalFunds += player.funds
    	}
    	
    	assert( (20.0 - totalFunds).abs < 0.001 )
    end

    def test_PlayerResourcesShouldNetToZeroWithAwardTiesToAllBiddersStrategy
    	totalFunds = 0.0

		gameShell = GameShell.new AwardTiesToAllBiddersStrategy.new

    	gameShell.eachPlayer { | player |
    		totalFunds += player.funds
    	}
    	
    	assert_equal 20.0, totalFunds
    	
        gameShell.executeManyGames

    	totalFunds = 0.0

    	gameShell.eachPlayer { | player |
    		totalFunds += player.funds
    	}
    	
    	assert( (20.0 - totalFunds).abs < 0.001 )
    end
    
    def test_ShouldSaveAndRestoreLearningState
		gameShell = GameShell.new FirstInListTieBreakStrategy.new, 5
		gameShell.executeManyGames		
		state = YAML.dump(gameShell.auctioneer)
		
		gameShell = GameShell.new FirstInListTieBreakStrategy.new, 5
		restored_auctioneer = YAML.load(state)

        assert_equal 2, countCorrectMoves(gameShell)
		gameShell.auctioneer = restored_auctioneer
        assert_equal 5, countCorrectMoves(gameShell)
    end

    def test_displayAuctions
        assert_equal "Pieces\tBid\tPlayer ID       \tAmount\tPos\tPieces\tFunds\n\n\n", @gameShell.displayAuctions
    end
    
    def test_executeCommandOptions
 
 		options = NimOptionValues.new
 		result = GameShell.executeCommandOptions options
		assert_no_match(/Pieces\tBid\tPlayer ID       \tAmount/, result)

 		options.displayAuctions = true
 		result = GameShell.executeCommandOptions options
		assert_match(/Pieces\tBid\tPlayer ID       \tAmount/, result)
        
    end
end

class TC_CompareTieBreakStrategies < Test::Unit::TestCase
    def countWeakLearningFailures tieBreakStrategy, pileSize = 10
        # We define success for our "weak" learning criteria to mean
        # that the system will always supply a bidder for a winning
        # position, and that said bidder will make the winning move.
        # We further require that no player will bid on a losing position.

        gameShell = GameShell.new(tieBreakStrategy, pileSize)

        gameShell.executeManyGames

        errorCount = gameShell.initialPileSize - countCorrectMoves(gameShell)
    end

    def countStrongLearningFailures tieBreakStrategy, pileSize = 10
        # We define success for our "strong" learning criteria to
        # mean that every player with a winning strategy remains
        # solvent, while every player with a losing strategy
        # is without funds.

        gameShell = GameShell.new(tieBreakStrategy, pileSize)

        gameShell.executeManyGames

        errorCount = 0

        gameShell.auctioneer.players.each { | player |
            piecesToTake  = player.moveStrategy.piecesToTake
            perfectPieces = perfectMove(player.bidStrategy.positionToBidOn)

            if (piecesToTake == perfectPieces)
                errorCount += 1 if player.funds < 1.0
            else
                errorCount += 1 if player.funds > 0.0
            end
        }

        errorCount
    end

    def test_BreakTiesByLotStrategyShouldWorkOnlyIntermittently
        # Demonstrates that BreakTiesByLotStrategy is not adequate.

        byLotStrategy = BreakTiesByLotStrategy.new

        # This works, but only by coincidence.
        srand 2
        assert_equal 0, countStrongLearningFailures(byLotStrategy)

        # Doesn't work! That is, bad strategies do not always go out of
        # business. This ought to have been obvious to us, since random
        # assignment might always be equivalent to our older
        # FirstInListTieBreakStrategy.
        srand 1
        assert_equal 1, countStrongLearningFailures(byLotStrategy)

        # It doesn't always produce learning that satisfies even our "weak"
        # criteria:
        srand 2

        # The first time is OK, by coincidence.
        assert_equal 0, countWeakLearningFailures(byLotStrategy)

        # Apparently stochastic strategies have pitfalls!
        assert_not_equal 0, countWeakLearningFailures(byLotStrategy)
    end

    def test_FirstInListTieBreakStrategyIsInadequate
        # This stratgegy satisfied only our "weak" criteria.

        firstInListStrategy = FirstInListTieBreakStrategy.new
        assert_equal 0, countWeakLearningFailures(firstInListStrategy)

        assert_not_equal 0, countStrongLearningFailures(firstInListStrategy)
    end

    def test_ShouldLearnUsingBreakTiesByTimeStrategy
        # BreakTiesByTimeStrategy seems best -- it satisfies both
        # "weak" and "strong" criteria. This is in fact misleading.

        byTimeStrategy = BreakTiesByTimeStrategy.new

        assert_equal 0, countWeakLearningFailures(byTimeStrategy)
        assert_equal 0, countStrongLearningFailures(byTimeStrategy)
    end

    def test_ShouldNotLearnUsingBreakTiesByTimeStrategyFor9Stones
        # BreakTiesByTimeStrategy is not in fact a good strategy.
        # It does not satisfy even "weak" criteria for some positions.
 
        byTimeStrategy = BreakTiesByTimeStrategy.new

        assert_not_equal 0, countWeakLearningFailures(byTimeStrategy, 9)
    end

    def test_ShouldLearnUsingAwardTiesToAllBiddersStrategy
        allBiddersStrategy = AwardTiesToAllBiddersStrategy.new

		for pileSize in 1..20
			assert_equal 0, countWeakLearningFailures(allBiddersStrategy, pileSize), "Weak learning failure for pileSize #{pileSize}"
			assert_equal 0, countStrongLearningFailures(allBiddersStrategy, pileSize), "Strong learning failure for pileSize #{pileSize}"
        end
	end
end

class TC_BreakTiesByLotStrategy < Test::Unit::TestCase
    def test_chooseWinner
        # Testing stochastic methods is tricky!

        # This test will fail on average every 2**39 runs,
        # or about one time in a half a trillion.
        # An alternate way to write the test would be to
        # generate the random values here and pass them in to
        # BreakTiesByLotStrategy.chooseWinner.

        s = BreakTiesByLotStrategy.new

        assert_equal([], s.chooseWinner([]))

        bidders = ['a', 'b']

        winners = {}

        40.times {
            winners[s.chooseWinner(bidders)[0]] = true
        }

        assert_equal 2, winners.length
        assert winners['a']
        assert winners['b']
    end
end

class TC_didLearn < Test::Unit::TestCase
	def setup
		@a1 = [1, 2, 3]
		@a2 = [1, 2, 3]
		@a3 = [1, 2, 2]
		@a4 = [1, 2, 4]
	end

    def test_didLearn
		assert !GameShell.didLearn(@a1, @a2)
		assert GameShell.didLearn(@a1, @a3)
		assert !GameShell.didLearn(@a1, @a4)
		assert GameShell.didLearn(@a1, [])
		assert GameShell.didLearn([], @a1)
    end
end

