Skip to content

Instantly share code, notes, and snippets.

@Metapyziks
Last active August 29, 2015 13:56
Show Gist options
  • Select an option

  • Save Metapyziks/9195636 to your computer and use it in GitHub Desktop.

Select an option

Save Metapyziks/9195636 to your computer and use it in GitHub Desktop.
Solver for the card flip minigame in Pokemon Heart Gold / Soul Silver.
//#define TEST
using System;
using System.Collections.Generic;
using System.Linq;
namespace CardFlipSolver
{
class Program
{
struct LineConstraint
{
public int Value;
public int Bombs;
}
class Card
{
private HashSet<int> _possibilities;
private double _bombProbability;
public IEnumerable<int> Possibilities { get { return _possibilities; } }
public bool MustBeBomb { get { return !MustBeValue && Possibilities.Count() == 1; } }
public bool MustBeValue { get { return !Possibilities.Contains(0); } }
public bool Revealed { get; private set; }
public bool Confirmed { get; private set; }
public double BombProbability
{
get { return MustBeBomb ? 1.0 : _bombProbability; }
set { _bombProbability = value; }
}
public double Expected { get; set; }
public int TrueValue
{
get
{
if (Revealed) return _possibilities.First();
throw new InvalidOperationException();
}
}
public Card()
{
_possibilities = new HashSet<int> { 0, 1, 2, 3 };
}
public void Reveal(int value)
{
if (_possibilities.Contains(value)) {
_possibilities = new HashSet<int> { value };
Revealed = true;
Confirmed = false;
}
}
public void Confirm()
{
Confirmed = true;
}
public bool Constrain(IEnumerable<int> constraint)
{
var diff = _possibilities
.Where(x => !constraint.Contains(x))
.ToArray();
if (diff.Count() == _possibilities.Count) {
throw new Exception();
}
foreach (var val in diff) {
_possibilities.Remove(val);
}
return diff.Length > 0;
}
}
static Card[] GetRow(Card[,] grid, int row)
{
return new[] { grid[row, 0], grid[row, 1], grid[row, 2], grid[row, 3], grid[row, 4] };
}
static Card[] GetCol(Card[,] grid, int col)
{
return new[] { grid[0, col], grid[1, col], grid[2, col], grid[3, col], grid[4, col] };
}
static IEnumerable<int[]> GetPermutations(int count, int bombs, int value, int min, int max)
{
int values = count - bombs;
if (value < min * values || value > max * values) return new int[0][];
if (bombs < 0 || bombs > count) return new int[0][];
if (value == 0 && bombs == 0) return new int[0][];
if (count == 1) {
return new[] { new[] { value } };
}
return Enumerable.Range(min, max + 1 - min)
.SelectMany(x => GetPermutations(count - 1, bombs - (x == 0 ? 1 : 0), value - x, min, max)
.Select(y => new[] { x }
.Concat(y)
.ToArray()))
.ToArray();
}
static int[][] GetPossibilities(LineConstraint constraint, Card[] cards)
{
constraint.Value -= cards.Where(x => x.Revealed).Sum(x => x.TrueValue);
constraint.Bombs -= cards.Where(x => x.MustBeBomb).Count();
var cardArray = cards.Where(x => !x.MustBeBomb && !x.Revealed).ToArray();
if (cardArray.Length == 0) return new[] { cards.Select(x => x.Revealed ? x.TrueValue : 0).ToArray() };
int count = cardArray.Length;
int max = cardArray.SelectMany(x => x.Possibilities).Max();
int min = cardArray.SelectMany(x => x.Possibilities).Min();
max = Math.Min(max, constraint.Value - (count - constraint.Bombs - 1));
min = Math.Max(min, constraint.Bombs == 0 ? 1 : 0);
var perms = GetPermutations(count, constraint.Bombs, constraint.Value, min, max)
.ToArray()
.Where(x => Enumerable.Range(0, count)
.All(y => cardArray[y].Possibilities.Contains(x[y])))
.ToArray();
if (perms.Any(x => perms.Any(y => x != y && Enumerable.Range(0, 5).All(i => x[i] == y[i])))) {
throw new Exception();
}
var final = new int[perms.Length][];
for (int i = 0; i < perms.Length; ++i) {
final[i] = new int[5];
for (int j = 0, k = 0; j < 5; ++j) {
if (cards[j].Revealed) {
final[i][j] = cards[j].TrueValue;
} else if (cards[j].MustBeBomb) {
final[i][j] = 0;
} else {
final[i][j] = perms[i][k++];
}
}
}
return final;
}
static void UpdateConstraint(LineConstraint constraint, Card[] cards)
{
var perms = GetPossibilities(constraint, cards);
var possible = Enumerable.Range(0, 5)
.Select(x => perms
.Select(y => y[x])
.Distinct()
.ToArray())
.ToArray();
for (int i = 0; i < 5; ++i) {
cards[i].Constrain(possible[i]);
}
}
static bool AutoReveal(Card[,] grid)
{
bool changed = false;
foreach (var card in grid) {
if (!card.Revealed && card.MustBeValue && card.Possibilities.Count() == 1 && card.Possibilities.First() == 1) {
card.Reveal(card.Possibilities.First());
changed = true;
}
}
return changed;
}
static int[,] SelectConfiguration(List<int[]>[] rows, int index)
{
var grid = new int[5, 5];
for (int r = 0; r < 5; ++r) {
int i = index % rows[r].Count;
var row = rows[r][i];
for (int c = 0; c < 5; ++c) {
grid[r, c] = row[c];
}
index /= rows[r].Count;
}
return grid;
}
static Random _rand = new Random();
static bool CalculateProbabilities(Card[,] grid, LineConstraint[] rows, LineConstraint[] cols, int level, out int minScore, out int maxScore, out int avgScore)
{
Console.WriteLine("Calculating probabilities: ");
int left = Console.CursorLeft;
var rowPoss = new List<int[]>[5];
var colPoss = new List<int[]>[5];
int count = 1;
for (int i = 0; i < 5; ++i) {
rowPoss[i] = GetPossibilities(rows[i], GetRow(grid, i)).ToList();
colPoss[i] = GetPossibilities(cols[i], GetCol(grid, i)).ToList();
count *= rowPoss[i].Count;
}
int limit = Math.Min(count, 5000000);
var valid = new List<int[,]>();
long totalScore = 0;
minScore = 0;
maxScore = 0;
int lastPercent = -1;
for (int i = 0; i < limit; ++i) {
var config = SelectConfiguration(rowPoss, limit == count ? i : _rand.Next(count));
int percent = ((i + 1) * 100) / limit;
if (percent > lastPercent) {
Console.CursorLeft = left;
Console.Write("{0}%", percent);
lastPercent = percent;
}
int score = Enumerable.Range(0, 5)
.SelectMany(r => Enumerable.Range(0, 5)
.Select(c => Math.Max(1, config[r, c])))
.Aggregate(1, (x, y) => x * y);
if (score < GetLevelMinScore(level) || score > GetLevelMaxScore(level)) {
continue;
}
if (Enumerable.Range(0, 5)
.All(c => colPoss[c]
.Any(col => Enumerable.Range(0, 5)
.All(r => col[r] == config[r, c])))) {
valid.Add(config);
if (score < minScore || minScore == 0) {
minScore = score;
}
if (score > maxScore) {
maxScore = score;
}
totalScore += score;
}
}
Console.WriteLine();
bool changed = false;
for (int r = 0; r < 5; ++r) {
for (int c = 0; c < 5; ++c) {
var prob = valid.Count(x => x[r, c] == 0) / (double) valid.Count;
var expected = valid.Sum(x => x[r, c]) / (double) valid.Count;
var card = grid[r, c];
if (limit != count) {
prob += (0.5 - prob) * 0.02;
} else {
changed = card.Constrain(card.Possibilities.Where(x => valid.Any(y => y[r, c] == x))) || changed;
}
grid[r, c].Expected = expected;
if (prob != grid[r, c].BombProbability) {
grid[r, c].BombProbability = prob;
}
}
}
avgScore = (int) (totalScore / valid.Count);
return changed;
}
static ConsoleColor GetCardColor(Card card, Card[,] grid)
{
if (card.Revealed) {
return card.Confirmed ? ConsoleColor.DarkGray : ConsoleColor.Yellow;
}
if (card.MustBeBomb) {
return ConsoleColor.Red;
}
if (card.MustBeValue) {
return ConsoleColor.Green;
}
var min = Enumerable.Range(0, 5)
.Select(r => Enumerable.Range(0, 5)
.Select(c => !grid[r, c].Revealed ? grid[r, c].BombProbability : 1.0)
.Min())
.Min();
if (card.BombProbability <= 1.1 * min) {
return ConsoleColor.Cyan;
}
if (card.BombProbability <= min + (1.0 - min) / 4.0) {
return ConsoleColor.DarkCyan;
}
return ConsoleColor.White;
}
static void PrintCard(Card card, Card[,] grid, int part)
{
Console.ForegroundColor = GetCardColor(card, grid);
switch (part) {
case 0:
Console.Write(" {0} {1} ",
!card.Revealed && !card.MustBeBomb && card.Possibilities.Contains(0) ? "x" : " ",
!card.Revealed && card.Possibilities.Contains(1) ? "1" : " ");
break;
case 1:
Console.Write(" {0} ",
card.Revealed ? " " + card.TrueValue.ToString() + " " :
card.MustBeBomb ? " X " :
card.MustBeValue ? " # " :
((int) (card.BombProbability * 100)).ToString("D2") + "%");
break;
case 2:
Console.Write(" {0} {1} ",
!card.Revealed && card.Possibilities.Contains(2) ? "2" : " ",
!card.Revealed && card.Possibilities.Contains(3) ? "3" : " ");
break;
default:
Console.ResetColor();
Console.Write("-----");
break;
}
Console.ResetColor();
if (part >= 3) Console.Write("+");
else Console.Write("|");
}
static void PrintGrid(Card[,] grid)
{
Console.Write("|");
for (int c = 0; c < 5; ++c) {
Console.Write(" {0} |", (c + 1).ToString());
}
Console.WriteLine();
Console.Write("+");
for (int c = 0; c < 5; ++c) {
Console.Write("-----+");
}
Console.WriteLine("-");
for (int r = 0; r < 5; ++r) {
for (int i = 0; i < 4; ++i) {
if (i < 3) {
Console.Write("|");
} else {
Console.Write("+");
}
for (int c = 0; c < 5; ++c) {
PrintCard(grid[r, c], grid, i);
if (c == 4) {
if (i == 3) {
Console.Write("-");
} else if (i == 1) {
Console.Write(r + 1);
}
}
}
Console.WriteLine();
}
}
Console.WriteLine();
}
static void PrintCardBorder(int r, int c, ConsoleColor color)
{
var orig = Console.CursorTop;
var top = Console.CursorTop - 5 * 4 - 3;
Console.ForegroundColor = color;
Console.CursorTop = top + 1 + r * 4;
Console.CursorLeft = c * 6;
Console.Write("+-----+");
for (int i = 0; i < 3; ++i) {
Console.CursorLeft -= 7;
Console.CursorTop += 1;
Console.Write("|");
Console.CursorLeft += 5;
Console.Write("|");
}
Console.CursorLeft -= 7;
Console.CursorTop += 1;
Console.Write("+-----+");
Console.ResetColor();
Console.CursorTop = orig;
Console.CursorLeft = 0;
}
static int GetLevelMinScore(int level)
{
switch (level) {
case 1:
return 16;
case 2:
return 32;
case 3:
return 64;
case 4:
return 128;
case 5:
return 256;
default:
return 512;
}
}
static int GetLevelMaxScore(int level)
{
switch (level) {
case 1:
return 64;
case 2:
return 128;
case 3:
return 256;
case 4:
return 512;
case 5:
return 1024;
default:
return 50000;
}
}
static int ReadNumber(String message, int min, int max)
{
int val;
do {
Console.Write("{0}: ", message);
} while (!int.TryParse(Console.ReadLine(), out val) || val < min || val > max);
return val;
}
static IEnumerable<Card> EnumerateGrid(Card[,] grid)
{
for (int r = 0; r < 5; ++r) {
for (int c = 0; c < 5; ++c) {
yield return grid[r, c];
}
}
}
static void GetCardLocation(Card[,] grid, Card card, out int r, out int c)
{
r = 0; c = 0;
for (r = 0; r < 5; ++r) {
for (c = 0; c < 5; ++c) {
if (grid[r, c] == card) return;
}
}
}
static void Main(string[] args)
{
Console.SetWindowSize(40, 24);
Console.SetBufferSize(40, 24);
#if TEST
var test = new int[] {
7, 1,
4, 3,
7, 1,
2, 3,
4, 2,
6, 1,
5, 2,
6, 1,
4, 3,
3, 3
};
#endif
var rows = new LineConstraint[5];
var cols = new LineConstraint[5];
var grid = new Card[5, 5];
while (true) {
Console.Clear();
for (int r = 0; r < 5; ++r) {
for (int c = 0; c < 5; ++c) {
grid[r, c] = new Card();
}
}
Console.Write("Level: ");
int level = int.Parse(Console.ReadLine());
for (int r = 0; r < 5; ++r) {
#if TEST
rows[r].Value = test[r * 2];
rows[r].Bombs = test[r * 2 + 1];
#else
rows[r].Value = ReadNumber(String.Format("Row {0} Value", r + 1), 0, 15);
rows[r].Bombs = ReadNumber(String.Format("Row {0} Bombs", r + 1), 0, 5);
#endif
}
for (int c = 0; c < 5; ++c) {
#if TEST
cols[c].Value = test[10 + c * 2];
cols[c].Bombs = test[10 + c * 2 + 1];
#else
cols[c].Value = ReadNumber(String.Format("Col {0} Value", c + 1), 0, 15);
cols[c].Bombs = ReadNumber(String.Format("Col {0} Bombs", c + 1), 0, 5);
#endif
}
#if TEST
//grid[0, 1].Reveal(2);
//grid[1, 0].Reveal(1);
//grid[1, 1].Reveal(1);
//grid[1, 2].Reveal(2);
//grid[1, 3].Reveal(3);
//grid[1, 4].Reveal(1);
//grid[4, 2].Reveal(2);
#endif
Console.WriteLine();
int top = Console.CursorTop;
int curRow = 0, curCol = 0;
var cancelled = false;
do {
int minScore, maxScore, avgScore;
Console.Clear();
do {
for (int i = 0; i < 5; ++i) {
UpdateConstraint(rows[i], GetRow(grid, i));
UpdateConstraint(cols[i], GetCol(grid, i));
}
} while (AutoReveal(grid) || CalculateProbabilities(grid, rows, cols, level, out minScore, out maxScore, out avgScore) || AutoReveal(grid));
Console.Clear();
PrintGrid(grid);
Console.WriteLine("Min: {0}, Max: {1}, Avg: {2}", minScore, maxScore, avgScore);
Console.CursorTop -= 1;
var bestArr = EnumerateGrid(grid)
.Where(x => !x.Revealed && !x.MustBeBomb)
.OrderBy(x => x.BombProbability)
.ToArray();
if (bestArr.Length > 0) {
var best = bestArr[0];
bestArr = bestArr
.Where(x => x.BombProbability == best.BombProbability)
.OrderByDescending(x => x.Expected)
.ToArray();
best = bestArr[0];
bestArr = bestArr
.Where(x => x.Expected == best.Expected)
.ToArray();
best = bestArr[_rand.Next(bestArr.Length)];
GetCardLocation(grid, best, out curRow, out curCol);
}
bool revealed = false;
do {
PrintCardBorder(curRow, curCol, ConsoleColor.Yellow);
switch (Console.ReadKey(true).Key) {
case ConsoleKey.LeftArrow:
if (curCol >= 1) {
PrintCardBorder(curRow, curCol, ConsoleColor.Gray);
curCol -= 1;
}
break;
case ConsoleKey.RightArrow:
if (curCol < 4) {
PrintCardBorder(curRow, curCol, ConsoleColor.Gray);
curCol += 1;
}
break;
case ConsoleKey.UpArrow:
if (curRow >= 1) {
PrintCardBorder(curRow, curCol, ConsoleColor.Gray);
curRow -= 1;
}
break;
case ConsoleKey.DownArrow:
if (curRow < 4) {
PrintCardBorder(curRow, curCol, ConsoleColor.Gray);
curRow += 1;
}
break;
case ConsoleKey.D1:
case ConsoleKey.NumPad1:
grid[curRow, curCol].Reveal(1);
revealed = true;
break;
case ConsoleKey.D2:
case ConsoleKey.NumPad2:
grid[curRow, curCol].Reveal(2);
revealed = true;
break;
case ConsoleKey.D3:
case ConsoleKey.NumPad3:
grid[curRow, curCol].Reveal(3);
revealed = true;
break;
case ConsoleKey.R:
cancelled = true;
break;
}
foreach (var card in EnumerateGrid(grid)) {
if (card.Revealed && !card.Confirmed) {
card.Confirm();
}
}
} while (!revealed && !cancelled);
} while (!cancelled);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment