summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoremkael <emkael@tlen.pl>2018-04-10 02:18:46 +0200
committeremkael <emkael@tlen.pl>2018-04-10 02:18:46 +0200
commit211428ebea89e08111354e6956e075967ee4f4af (patch)
treed3a2d2a6534154e1f01d1bcb034d1cec5f5be0ca
Initial .NET codebase
-rw-r--r--.gitignore1
-rw-r--r--Program.cs116
-rw-r--r--src/BCalcWrapper.cs44
-rw-r--r--src/DDTable.cs156
-rw-r--r--src/PBNBoard.cs390
-rw-r--r--src/PBNFile.cs75
-rw-r--r--src/ParContract.cs227
-rw-r--r--src/ParScore.cs233
8 files changed, 1242 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..61c2094
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.suo
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..ea1eab3
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Windows.Forms;
+using System.IO;
+
+namespace BCDD
+{
+ class Program
+ {
+ static List<String> getFiles(string[] args)
+ {
+ List<String> filenames = new List<String>();
+ foreach (String arg in args)
+ {
+ if (File.Exists(arg))
+ {
+ filenames.Add(arg);
+ }
+ }
+ if (filenames.Count == 0)
+ {
+ OpenFileDialog fd = new OpenFileDialog();
+ fd.Multiselect = true;
+ fd.Filter = "PBN files (*.pbn)|*.pbn|All files (*.*)|*.*";
+ if (fd.ShowDialog() == DialogResult.OK)
+ {
+ filenames = new List<String>(fd.FileNames);
+ }
+ }
+ return filenames;
+ }
+
+ [STAThread]
+ static void Main(string[] args)
+ {
+ List<String> files = Program.getFiles(args);
+ List<String> errors = new List<String>();
+ if (files.Count > 0)
+ {
+ foreach (String filename in files)
+ {
+ try
+ {
+ Console.WriteLine("Analyzing " + filename);
+ PBNFile file = new PBNFile(filename);
+ foreach (PBNBoard board in file.Boards)
+ {
+ DDTable table = new DDTable(board);
+ String boardNo;
+ try
+ {
+ boardNo = board.GetNumber();
+ }
+ catch (FieldNotFoundException)
+ {
+ boardNo = "?";
+ }
+ try
+ {
+ int[,] ddTable = table.GetDDTable();
+ if (ddTable != null)
+ {
+ Console.WriteLine("Board " + boardNo);
+ DDTable.PrintTable(ddTable);
+ ParScore par = new ParScore(board);
+ ParContract contract = par.GetParContract(ddTable);
+ Console.WriteLine(contract);
+ Console.WriteLine();
+ board.SaveDDTable(ddTable);
+ board.SaveParContract(contract);
+ file.WriteBoard(board);
+ }
+ else
+ {
+ String error = "unable to determine DD table for board " + boardNo;
+ errors.Add(String.Format("[{0}] {1}", filename, error));
+ Console.WriteLine("ERROR: " + error);
+ }
+ }
+ catch (DllNotFoundException)
+ {
+ throw;
+ }
+ catch (Exception e)
+ {
+ errors.Add(String.Format("[{0}:{1}] {2}", filename, boardNo, e.Message));
+ Console.WriteLine("ERROR: " + e.Message);
+ }
+ }
+ file.Save();
+ }
+ catch (DllNotFoundException e)
+ {
+ errors.Add("libbcalcdds.dll could not be loaded - make sure it's present in application directory!");
+ Console.WriteLine("ERROR: " + e.Message);
+ break;
+ }
+ catch (Exception e)
+ {
+ errors.Add(e.Message);
+ Console.WriteLine("ERROR: " + e.Message);
+ }
+ }
+ if (errors.Count > 0) {
+ Console.WriteLine("Following ERRORs occured:");
+ foreach (String error in errors) {
+ Console.WriteLine(error);
+ }
+ Console.WriteLine("Press any key to continue...");
+ Console.ReadKey();
+ }
+ }
+ }
+ }
+}
diff --git a/src/BCalcWrapper.cs b/src/BCalcWrapper.cs
new file mode 100644
index 0000000..d2d0aa2
--- /dev/null
+++ b/src/BCalcWrapper.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace BCDD
+{
+ /// <summary>
+ /// Wrapper class for libbcalcDDS.ddl.
+ /// </summary>
+ class BCalcWrapper
+ {
+ public static char[] DENOMINATIONS = { 'C', 'D', 'H', 'S', 'N' };
+ public static char[] PLAYERS = { 'N', 'E', 'S', 'W' };
+
+ /// <remarks>http://bcalc.w8.pl/API_C/bcalcdds_8h.html#a8f522e85482fe383bebd963e873897f5</remarks>
+ [DllImport(@"libbcalcdds.dll", CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr bcalcDDS_new(IntPtr format, IntPtr hands, Int32 trump, Int32 leader);
+
+ /// <remarks>http://bcalc.w8.pl/API_C/bcalcdds_8h.html#a369ce661d027bef3f717967e42bf8b33</remarks>
+ [DllImport(@"libbcalcdds.dll", CallingConvention = CallingConvention.Cdecl)]
+ public static extern Int32 bcalcDDS_getTricksToTake(IntPtr solver);
+
+ /// <remarks>http://bcalc.w8.pl/API_C/bcalcdds_8h.html#a8998a1eb1ca25de2e07448381ce63261</remarks>
+ [DllImport(@"libbcalcdds.dll", CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr bcalcDDS_getLastError(IntPtr solver);
+
+ /// <remarks>http://bcalc.w8.pl/API_C/bcalcdds_8h.html#a4a68da83bc7da4663e2257429539912d</remarks>
+ [DllImport(@"libbcalcdds.dll", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void bcalcDDS_delete(IntPtr solver);
+
+ /// <remarks>http://bcalc.w8.pl/API_C/bcalcdds_8h.html#a88fba3432e66efa5979bbc9e1f044164</remarks>
+ [DllImport(@"libbcalcdds.dll", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void bcalcDDS_setTrumpAndReset(IntPtr solver, Int32 trump);
+
+ /// <remarks>http://bcalc.w8.pl/API_C/bcalcdds_8h.html#a616031c1e1d856c4aac14390693adb4c</remarks>
+ [DllImport(@"libbcalcdds.dll", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void bcalcDDS_setPlayerOnLeadAndReset(IntPtr solver, Int32 player);
+
+ /// <remarks>http://bcalc.w8.pl/API_C/bcalcdds_8h.html#a6977a3b789bdf64eb2da9cbdb8b8fc39</remarks>
+ public static Int32 bcalc_declarerToLeader(Int32 player)
+ {
+ return (player + 1) & 3;
+ }
+ }
+}
diff --git a/src/DDTable.cs b/src/DDTable.cs
new file mode 100644
index 0000000..c2ffcd1
--- /dev/null
+++ b/src/DDTable.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
+
+namespace BCDD
+{
+ class DDTableInvalidException : FieldNotFoundException
+ {
+ public DDTableInvalidException() : base() { }
+ public DDTableInvalidException(String msg) : base(msg) { }
+ }
+
+ class DDTable
+ {
+ private PBNBoard board;
+
+ private int[,] getEmptyTable()
+ {
+ int[,] result = new int[4, 5];
+ for (int i = 0; i < 4; i++)
+ {
+ for (int j = 0; j < 5; j++)
+ {
+ result[i, j] = -1;
+ }
+ }
+ return result;
+ }
+
+ private int[,] validateTable(int[,] table)
+ {
+ foreach (int t in table)
+ {
+ if (t > 13 || t < 0)
+ {
+ throw new DDTableInvalidException("Invalid number of tricks: " + t.ToString());
+ }
+ }
+ return table;
+ }
+
+ public DDTable(PBNBoard board)
+ {
+ this.board = board;
+ }
+
+ private static bool bannerDisplayed = false;
+
+ public int[,] GetBCalcTable()
+ {
+ if (!DDTable.bannerDisplayed)
+ {
+ Console.WriteLine("Double dummy analysis provided by BCalc.");
+ Console.WriteLine("BCalc is awesome, check it out: http://bcalc.w8.pl");
+ DDTable.bannerDisplayed = true;
+ }
+ int[,] result = this.getEmptyTable();
+ String deal = this.board.GetLayout();
+ IntPtr solver = BCalcWrapper.bcalcDDS_new(Marshal.StringToHGlobalAnsi("PBN"), Marshal.StringToHGlobalAnsi(deal), 0, 0);
+ for (int denom = 0; denom < 5; denom++)
+ {
+ BCalcWrapper.bcalcDDS_setTrumpAndReset(solver, denom);
+ for (int player = 0; player < 4; player++)
+ {
+ BCalcWrapper.bcalcDDS_setPlayerOnLeadAndReset(solver, BCalcWrapper.bcalc_declarerToLeader(player));
+ result[player, denom] = 13 - BCalcWrapper.bcalcDDS_getTricksToTake(solver);
+ String error = Marshal.PtrToStringAnsi(BCalcWrapper.bcalcDDS_getLastError(solver));
+ if (error != null)
+ {
+ throw new DDTableInvalidException("BCalc error: " + error);
+ }
+ }
+ }
+ BCalcWrapper.bcalcDDS_delete(solver);
+ return this.validateTable(result);
+ }
+
+ public int[,] GetJFRTable()
+ {
+ int[,] result = this.getEmptyTable();
+ String ability = this.board.GetAbility();
+ MatchCollection abilities = this.board.ValidateAbility(ability);
+ foreach (Match playerAbility in abilities)
+ {
+ char player = playerAbility.Groups[1].Value[0];
+ int playerID = Array.IndexOf(BCalcWrapper.PLAYERS, player);
+ int denomID = 4;
+ foreach (char tricks in playerAbility.Groups[2].Value.ToCharArray())
+ {
+ result[playerID, denomID] = (tricks > '9') ? (tricks - 'A' + 10) : (tricks - '0');
+ denomID--;
+ }
+ }
+ return this.validateTable(result);
+ }
+
+ public int[,] GetPBNTable()
+ {
+ List<String> table = this.board.GetOptimumResultTable();
+ List<Match> parsedTable = this.board.ValidateOptimumResultTable(table);
+ int[,] result = this.getEmptyTable();
+ foreach (Match lineMatch in parsedTable)
+ {
+ char player = lineMatch.Groups[1].Value[0];
+ char denom = lineMatch.Groups[2].Value[0];
+ int tricks = Int16.Parse(lineMatch.Groups[3].Value);
+ int playerID = Array.IndexOf(BCalcWrapper.PLAYERS, player);
+ int denomID = Array.IndexOf(BCalcWrapper.DENOMINATIONS, denom);
+ result[playerID, denomID] = tricks;
+ }
+ return this.validateTable(result);
+ }
+
+ public int[,] GetDDTable()
+ {
+ try
+ {
+ return this.GetJFRTable();
+ }
+ catch (FieldNotFoundException)
+ {
+ try
+ {
+ return this.GetPBNTable();
+ }
+ catch (FieldNotFoundException)
+ {
+ return this.GetBCalcTable();
+ }
+ }
+ }
+
+ public static void PrintTable(int[,] ddTable)
+ {
+ foreach (char header in BCalcWrapper.DENOMINATIONS)
+ {
+ Console.Write('\t');
+ Console.Write(header);
+ }
+ Console.WriteLine();
+ for (int i = 0; i < 4; i++)
+ {
+ Console.Write(BCalcWrapper.PLAYERS[i]);
+ for (int j = 0; j < 5; j++)
+ {
+ Console.Write('\t');
+ Console.Write(ddTable[i, j].ToString());
+ }
+ Console.WriteLine();
+ }
+ }
+ }
+}
diff --git a/src/PBNBoard.cs b/src/PBNBoard.cs
new file mode 100644
index 0000000..7578fbe
--- /dev/null
+++ b/src/PBNBoard.cs
@@ -0,0 +1,390 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace BCDD
+{
+ class PBNField
+ {
+ public String Key;
+ public String Value;
+ public String RawField;
+
+ public PBNField() { }
+
+ public PBNField(String key, String value)
+ {
+ this.Key = key;
+ this.Value = value;
+ this.RawField = String.Format("[{0} \"{1}\"]", this.Key, this.Value);
+ }
+
+ public PBNField(String rawData)
+ {
+ this.RawField = rawData;
+ }
+ }
+
+ class FieldNotFoundException : Exception
+ {
+ public FieldNotFoundException() : base() { }
+ public FieldNotFoundException(String msg) : base(msg) { }
+ }
+
+ class PBNBoard
+ {
+ public List<PBNField> Fields;
+
+ private bool? hasOptimumResultTable = null;
+ private bool? hasAbility = null;
+
+ private static Regex linePattern = new Regex(@"\[(.*) ""(.*)""\]");
+ private static Regex abilityPattern = new Regex(@"\b([NESW]):([0-9A-D]{5})\b");
+ private static Regex optimumResultTablePattern = new Regex(@"^([NESW])\s+([CDHSN])T?\s+(\d+)$");
+
+ public PBNBoard(List<string> lines)
+ {
+ this.Fields = new List<PBNField>();
+ foreach (String line in lines)
+ {
+ PBNField field = new PBNField();
+ field.RawField = line;
+ Match lineParse = PBNBoard.linePattern.Match(line);
+ if (lineParse.Success)
+ {
+ field.Key = lineParse.Groups[1].Value;
+ field.Value = lineParse.Groups[2].Value;
+ }
+ this.Fields.Add(field);
+ }
+ }
+
+ public bool HasField(String key)
+ {
+ foreach (PBNField field in this.Fields)
+ {
+ if (key.Equals(field.Key))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public String GetField(String key)
+ {
+ foreach (PBNField field in this.Fields)
+ {
+ if (key.Equals(field.Key))
+ {
+ return field.Value;
+ }
+ }
+ throw new FieldNotFoundException(key + " field not found");
+ }
+
+ public void DeleteField(String key)
+ {
+ List<PBNField> toRemove = new List<PBNField>();
+ foreach (PBNField field in this.Fields)
+ {
+ if (key.Equals(field.Key))
+ {
+ toRemove.Add(field);
+ }
+ }
+ foreach (PBNField remove in toRemove)
+ {
+ this.Fields.Remove(remove);
+ }
+ }
+
+ public String GetEvent()
+ {
+ return this.GetField("Event");
+ }
+
+ public void WriteEvent(String name)
+ {
+ for (int i = 0; i < this.Fields.Count; i++)
+ {
+ if ("Board".Equals(this.Fields[i].Key))
+ {
+ this.Fields.Insert(i, new PBNField("Event", name));
+ break;
+ }
+ }
+ }
+
+ public String GetLayout()
+ {
+ return this.GetField("Deal");
+ }
+
+ public String GetNumber()
+ {
+ return this.GetField("Board");
+ }
+
+ public String GetVulnerable()
+ {
+ return this.GetField("Vulnerable");
+ }
+
+ public String GetDealer()
+ {
+ return this.GetField("Dealer");
+ }
+
+ public MatchCollection ValidateAbility(String ability)
+ {
+ MatchCollection matches = PBNBoard.abilityPattern.Matches(ability);
+ if (matches.Count != 4)
+ {
+ this.hasAbility = false;
+ throw new DDTableInvalidException("Invalid Ability line: " + ability);
+ }
+ List<String> players = new List<String>();
+ foreach (Match match in matches)
+ {
+ if (players.Contains(match.Groups[1].Value))
+ {
+ this.hasAbility = false;
+ throw new DDTableInvalidException("Duplicate entry in Ability: " + match.Groups[0].Value);
+ }
+ else
+ {
+ players.Add(match.Groups[1].Value);
+ }
+ }
+ this.hasAbility = true;
+ return matches;
+ }
+
+ public String GetAbility()
+ {
+ return this.GetField("Ability");
+ }
+
+ public void DeleteAbility()
+ {
+ this.DeleteField("Ability");
+ }
+
+ public void WriteAbility(int[,] ddTable)
+ {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < 4; i++)
+ {
+ sb.Append(BCalcWrapper.PLAYERS[i]);
+ sb.Append(':');
+ for (int j = 4; j >= 0; j--)
+ {
+ sb.Append((char)(ddTable[i, j] > 9 ? 'A' + ddTable[i, j] - 10 : ddTable[i, j] + '0'));
+ }
+ if (i < 3)
+ {
+ sb.Append(' ');
+ }
+ }
+ String abilityStr = sb.ToString();
+ this.Fields.Add(new PBNField("Ability", abilityStr));
+ }
+
+ public String GetMinimax()
+ {
+ return this.GetField("Minimax");
+ }
+
+ public void DeleteMinimax()
+ {
+ this.DeleteField("Minimax");
+ }
+
+ public void WriteMinimax(ParContract contract)
+ {
+ String minimax;
+ if (contract.Score == 0)
+ {
+ minimax = "7NS0";
+ }
+ else
+ {
+ minimax = String.Format("{0}{1}{2}{3}{4}", contract.Level, contract.Denomination, contract.Doubled ? "D" : "", contract.Declarer, contract.Score);
+ }
+ this.Fields.Add(new PBNField("Minimax", minimax));
+ }
+
+ public String GetOptimumScore()
+ {
+ return this.GetField("OptimumScore");
+ }
+
+ public void DeleteOptimumScore()
+ {
+ this.DeleteField("OptimumScore");
+ }
+
+ public void WriteOptimumScore(ParContract contract)
+ {
+ this.Fields.Add(new PBNField("OptimumScore", String.Format("NS {0}", contract.Score)));
+ }
+
+ public String GetOptimumResult()
+ {
+ return this.GetField("OptimumResult");
+ }
+
+ public List<Match> ValidateOptimumResultTable(List<String> table)
+ {
+ List<Match> matches = new List<Match>();
+ List<String> duplicates = new List<String>();
+ foreach (String line in table)
+ {
+ Match match = PBNBoard.optimumResultTablePattern.Match(line);
+ if (!match.Success)
+ {
+ this.hasOptimumResultTable = false;
+ throw new DDTableInvalidException("Invalid OptimumResultTable line: " + line);
+ }
+ String position = match.Groups[1].Value + " - " + match.Groups[2].Value;
+ if (duplicates.Contains(position))
+ {
+ this.hasOptimumResultTable = false;
+ throw new DDTableInvalidException("Duplicate OptimumResultTable line: " + line);
+ }
+ else
+ {
+ duplicates.Add(position);
+ }
+ matches.Add(match);
+ }
+ this.hasOptimumResultTable = true;
+ return matches;
+ }
+
+ public List<String> GetOptimumResultTable()
+ {
+ bool fieldFound = false;
+ List<String> result = new List<String>();
+ foreach (PBNField field in this.Fields)
+ {
+ if ("OptimumResultTable".Equals(field.Key))
+ {
+ fieldFound = true;
+ }
+ else
+ {
+ if (fieldFound)
+ {
+ if (field.Key == null)
+ {
+ result.Add(field.RawField);
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+ }
+ if (!fieldFound)
+ {
+ this.hasOptimumResultTable = false;
+ throw new FieldNotFoundException("OptimumResultTable field not found");
+ }
+ return result;
+ }
+
+ public void DeleteOptimumResultTable()
+ {
+ bool fieldFound = false;
+ List<PBNField> toRemove = new List<PBNField>();
+ foreach (PBNField field in this.Fields)
+ {
+ if ("OptimumResultTable".Equals(field.Key))
+ {
+ fieldFound = true;
+ toRemove.Add(field);
+ }
+ else
+ {
+ if (fieldFound)
+ {
+ if (field.Key == null)
+ {
+ toRemove.Add(field);
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+ }
+ foreach (PBNField remove in toRemove)
+ {
+ this.Fields.Remove(remove);
+ }
+ }
+
+ public void WriteOptimumResultTable(int[,] ddTable)
+ {
+ this.Fields.Add(new PBNField("OptimumResultTable", @"Declarer;Denomination\2R;Result\2R"));
+ for (int i = 0; i < 4; i++)
+ {
+ for (int j = 0; j < 5; j++)
+ {
+ this.Fields.Add(new PBNField(String.Format("{0} {1}{2} {3}", BCalcWrapper.PLAYERS[i], BCalcWrapper.DENOMINATIONS[j], (BCalcWrapper.DENOMINATIONS[j] == 'N') ? "T" : "", ddTable[i, j])));
+ }
+ }
+ }
+
+ public void SaveParContract(ParContract contract)
+ {
+ this.DeleteOptimumScore();
+ this.WriteOptimumScore(contract); // we're not writing DDS custom fields, just parse them
+ this.DeleteMinimax();
+ this.WriteMinimax(contract);
+ }
+
+ public void SaveDDTable(int[,] ddTable)
+ {
+ if (this.hasOptimumResultTable == null)
+ {
+ try
+ {
+ List<Match> optimumResultTable = this.ValidateOptimumResultTable(this.GetOptimumResultTable());
+ this.hasOptimumResultTable = true;
+ }
+ catch (FieldNotFoundException)
+ {
+ this.hasOptimumResultTable = false;
+ }
+ }
+ if (this.hasOptimumResultTable == false)
+ {
+ this.DeleteOptimumResultTable();
+ this.WriteOptimumResultTable(ddTable);
+ }
+ if (this.hasAbility == null)
+ {
+ try
+ {
+ MatchCollection ability = this.ValidateAbility(this.GetAbility());
+ this.hasAbility = true;
+ }
+ catch (FieldNotFoundException)
+ {
+ this.hasAbility = false;
+ }
+ }
+ if (this.hasAbility == false)
+ {
+ this.DeleteAbility();
+ this.WriteAbility(ddTable);
+ }
+ }
+ }
+}
diff --git a/src/PBNFile.cs b/src/PBNFile.cs
new file mode 100644
index 0000000..b26f84c
--- /dev/null
+++ b/src/PBNFile.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.IO;
+
+namespace BCDD
+{
+ class PBNFile
+ {
+ public List<PBNBoard> Boards;
+
+ private String filename;
+ private String tmpFileName;
+
+ StreamWriter outputFile;
+
+ public PBNFile(String filename)
+ {
+ this.filename = filename;
+ this.Boards = new List<PBNBoard>();
+ String[] contents = File.ReadAllLines(this.filename).Select(l => l.Trim()).ToArray();
+ List<String> lines = new List<String>();
+ foreach (String line in contents)
+ {
+ if (line.Length == 0)
+ {
+ if (lines.Count > 0)
+ {
+ this.Boards.Add(new PBNBoard(lines));
+ lines = new List<String>();
+ }
+ }
+ else
+ {
+ lines.Add(line);
+ }
+ }
+ if (lines.Count > 0)
+ {
+ this.Boards.Add(new PBNBoard(lines));
+ }
+ if (!this.Boards[0].HasField("Event"))
+ {
+ this.Boards[0].WriteEvent("");
+ }
+ }
+
+ public void WriteBoard(PBNBoard board)
+ {
+ if (this.outputFile == null)
+ {
+ this.tmpFileName = Path.GetTempFileName();
+ this.outputFile = new StreamWriter(new FileStream(this.tmpFileName, FileMode.Create), Encoding.UTF8);
+ }
+ foreach (PBNField field in board.Fields)
+ {
+ this.outputFile.WriteLine(field.RawField);
+ }
+ this.outputFile.WriteLine();
+ }
+
+ public void Save()
+ {
+ if (this.outputFile == null)
+ {
+ throw new IOException("No boards written to PBN file, unable to save it.");
+ }
+ this.outputFile.Flush();
+ this.outputFile.Close();
+ File.Delete(this.filename);
+ File.Move(this.tmpFileName, this.filename);
+ }
+ }
+}
diff --git a/src/ParContract.cs b/src/ParContract.cs
new file mode 100644
index 0000000..9255ca5
--- /dev/null
+++ b/src/ParContract.cs
@@ -0,0 +1,227 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace BCDD
+{
+ class ParContract
+ {
+ public int Level = 0;
+ public char Denomination;
+ public char Declarer;
+ public bool Doubled = false;
+ public int Score = 0;
+
+ public ParContract() { }
+
+ public ParContract(int level, char denom, char declarer, bool doubled, int score)
+ {
+ this.Level = level;
+ this.Denomination = denom;
+ this.Declarer = declarer;
+ this.Doubled = doubled;
+ this.Score = score;
+ }
+
+ public ParContract Validate()
+ {
+ if (this.Score == 0)
+ {
+ return this;
+ }
+ if (this.Level < 1 || this.Level > 7)
+ {
+ throw new ParScoreInvalidException("Invalid par contract level: " + this.Level.ToString());
+ }
+ if (!"CDHSN".Contains(this.Denomination))
+ {
+ throw new ParScoreInvalidException("Invalid par contract denomination: " + this.Denomination);
+ }
+ if (!"NESW".Contains(this.Declarer))
+ {
+ throw new ParScoreInvalidException("Invalid par contract declarer: " + this.Declarer);
+ }
+ return this;
+ }
+
+ override public String ToString()
+ {
+ if (this.Score == 0)
+ {
+ return "PASS";
+ }
+ String contract = this.Level.ToString() + this.Denomination;
+ String risk = this.Doubled ? "x" : "";
+ String declarer = " " + this.Declarer;
+ String result = " " + this.Score.ToString("+#;-#;0");
+ return contract + risk + declarer + result;
+ }
+
+ public override bool Equals(object other)
+ {
+ ParContract obj = (ParContract)(other);
+ return this.Level == obj.Level && this.Denomination == obj.Denomination && this.Score == obj.Score;
+ }
+
+ public override int GetHashCode()
+ {
+ return this.Score + this.Level + 10000 * this.Denomination;
+ }
+
+ public int CalculateScore(int tricks, bool vulnerable = false)
+ {
+ if (this.Level == 0)
+ {
+ return 0;
+ }
+ int score = 0;
+ if (this.Level + 6 > tricks)
+ {
+ int undertricks = this.Level + 6 - tricks;
+ if (this.Doubled)
+ {
+ do
+ {
+ if (undertricks == 1) // first undertrick: 100 non-vul, 200 vul
+ {
+ score -= vulnerable ? 200 : 100;
+ }
+ else
+ {
+ if (undertricks <= 3 && !vulnerable) // second non-vul undertrick: 200
+ {
+ score -= 200;
+ }
+ else // further undertricks: 300
+ {
+ score -= 300;
+ }
+ }
+ undertricks--;
+ }
+ while (undertricks > 0);
+ }
+ else
+ {
+ score = vulnerable ? -100 : -50;
+ score *= undertricks;
+ }
+ }
+ else
+ {
+ int parTricks = this.Level;
+ do
+ {
+ if (this.Denomination == 'N' && parTricks == 1) // first non-trump trick: 40
+ {
+ score += 40;
+ }
+ else // other tricks
+ {
+ switch (this.Denomination)
+ {
+ case 'N':
+ case 'S':
+ case 'H':
+ score += 30;
+ break;
+ case 'D':
+ case 'C':
+ score += 20;
+ break;
+ }
+ }
+ parTricks--;
+ }
+ while (parTricks > 0);
+ if (this.Doubled)
+ {
+ score *= 2;
+ }
+ score += (score >= 100) ? (vulnerable ? 500 : 300) : 50; // game premium
+ if (this.Level == 7) // grand slam premium
+ {
+ score += vulnerable ? 1500 : 1000;
+ }
+ else if (this.Level == 6) // small slam premium
+ {
+ score += vulnerable ? 750 : 500;
+ }
+ if (this.Doubled)
+ {
+ score += 50;
+ }
+ int overtricks = tricks - this.Level - 6;
+ score += this.Doubled
+ ? (vulnerable ? 200 : 100) * overtricks // (re-)double overtricks: 100/200/200/400
+ : overtricks * ((this.Denomination == 'C' || this.Denomination == 'D') ? 20 : 30); // undoubled overtricks
+ }
+ if (this.Declarer == 'E' || this.Declarer == 'W')
+ {
+ score = -score;
+ }
+ return score;
+ }
+
+ public bool Higher(ParContract obj)
+ {
+ return (this.Level > obj.Level
+ || (this.Level == obj.Level
+ && Array.IndexOf(BCalcWrapper.DENOMINATIONS, this.Denomination) > Array.IndexOf(BCalcWrapper.DENOMINATIONS, obj.Denomination)));
+ }
+
+ public ParContract GetDefense(int[,] ddTable, bool vulnerable)
+ {
+ int declarerIndex = Array.IndexOf(BCalcWrapper.PLAYERS, this.Declarer);
+ int denominationIndex = Array.IndexOf(BCalcWrapper.DENOMINATIONS, this.Denomination);
+ if (this.Level != 0 && this.Level + 6 <= ddTable[declarerIndex, denominationIndex])
+ {
+ List<int> defendersIndexes = new List<int>();
+ defendersIndexes.Add((declarerIndex + 1) & 3);
+ defendersIndexes.Add((declarerIndex + 3) & 3);
+ List<ParContract> possibleDefense = new List<ParContract>();
+ int scoreSquared = this.Score * this.Score;
+ for (int i = 0; i < 5; i++)
+ {
+ int level = this.Level;
+ if (i <= denominationIndex)
+ {
+ level++;
+ }
+ if (level <= 7)
+ {
+ foreach (int defender in defendersIndexes)
+ {
+ if (level + 6 > ddTable[defender, i])
+ {
+ ParContract defense = new ParContract(level, BCalcWrapper.DENOMINATIONS[i], BCalcWrapper.PLAYERS[defender], true, 0);
+ defense.Score = defense.CalculateScore(ddTable[defender, i], vulnerable);
+ if (scoreSquared > this.Score * defense.Score)
+ {
+ possibleDefense.Add(defense);
+ }
+ }
+ }
+ }
+ }
+ if (possibleDefense.Count > 0)
+ {
+ possibleDefense.Sort((x, y) => Math.Abs(x.Score - this.Score).CompareTo(Math.Abs(y.Score - this.Score)));
+ ParContract optimumDefense = possibleDefense.Last();
+ possibleDefense = possibleDefense.FindAll(x => x.Score == optimumDefense.Score);
+ foreach (ParContract defense in possibleDefense)
+ {
+ // Lowest from the most profitable sacrifices
+ if (optimumDefense.Higher(defense))
+ {
+ optimumDefense = defense;
+ }
+ }
+ return optimumDefense;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/ParScore.cs b/src/ParScore.cs
new file mode 100644
index 0000000..068be90
--- /dev/null
+++ b/src/ParScore.cs
@@ -0,0 +1,233 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace BCDD
+{
+ class ParScoreInvalidException : FieldNotFoundException
+ {
+ public ParScoreInvalidException() : base() { }
+ public ParScoreInvalidException(String msg) : base(msg) { }
+ }
+
+ class ParScore
+ {
+ private PBNBoard board;
+ private static Regex pbnContractPattern = new Regex(@"(\d)([CDHSN])(X?)\s+([NESW])");
+ private static Regex pbnScorePattern = new Regex(@"(NS|EW)\s+(-?\d})");
+ private static Regex jfrContractPattern = new Regex(@"^(\d)([CDHSN])(D?)([NESW])(-?\d+)$");
+
+ public ParScore(PBNBoard board)
+ {
+ this.board = board;
+ }
+
+ public ParContract GetPBNParContract()
+ {
+ String contractField = this.board.GetOptimumResult();
+ if ("Pass".Equals(contractField))
+ {
+ return new ParContract();
+ }
+ Match contractMatch = ParScore.pbnContractPattern.Match(contractField);
+ if (!contractMatch.Success)
+ {
+ throw new ParScoreInvalidException("Invalid format for OptimumResult field: " + contractField);
+ }
+ String scoreField = this.board.GetOptimumScore();
+ Match scoreMatch = ParScore.pbnScorePattern.Match(scoreField);
+ if (!scoreMatch.Success)
+ {
+ throw new ParScoreInvalidException("Invalid format for OptimumScore field: " + scoreField);
+ }
+ int score = Int16.Parse(scoreMatch.Groups[2].Value);
+ if ("EW".Equals(scoreMatch.Groups[1].Value))
+ {
+ score = -score;
+ }
+ ParContract contract = new ParContract(Int16.Parse(contractMatch.Groups[1].Value),
+ contractMatch.Groups[2].Value[0],
+ contractMatch.Groups[4].Value[0],
+ "X".Equals(contractMatch.Groups[3].Value),
+ score);
+ return contract.Validate();
+ }
+
+ public ParContract GetJFRParContract()
+ {
+ String parString = this.board.GetMinimax();
+ Match parMatch = ParScore.jfrContractPattern.Match(parString);
+ if (!parMatch.Success)
+ {
+ throw new ParScoreInvalidException("Invalid format for Minimax field: " + parString);
+ }
+ if ("0".Equals(parMatch.Groups[4].Value))
+ {
+ return new ParContract(); // pass-out
+ }
+ ParContract contract = new ParContract(Int16.Parse(parMatch.Groups[1].Value),
+ parMatch.Groups[2].Value[0],
+ parMatch.Groups[4].Value[0],
+ "D".Equals(parMatch.Groups[3].Value),
+ Int16.Parse(parMatch.Groups[5].Value));
+ return contract.Validate();
+ }
+
+ private bool determineVulnerability(String vulnerability, char declarer)
+ {
+ vulnerability = vulnerability.ToUpper();
+ return "ALL".Equals(vulnerability) || "BOTH".Equals(vulnerability)
+ || (!"LOVE".Equals(vulnerability) && !"NONE".Equals(vulnerability) && vulnerability.Contains(declarer));
+ }
+
+ private ParContract getHighestMakeableContract(int[,] ddTable, bool forNS = true, bool forEW = true)
+ {
+ ParContract contract = new ParContract();
+ int tricks = 0;
+ for (int i = 3; i >= 0; i--)
+ {
+ if ((i % 2 == 0 && forNS)
+ || (i % 2 == 1 && forEW))
+ {
+ for (int j = 0; j < 5; j++)
+ {
+ int level = ddTable[i, j] - 6;
+ if (level > contract.Level
+ || (level == contract.Level && j > Array.IndexOf(BCalcWrapper.DENOMINATIONS, contract.Denomination)))
+ {
+ contract.Level = level;
+ contract.Denomination = BCalcWrapper.DENOMINATIONS[j];
+ contract.Declarer = BCalcWrapper.PLAYERS[i];
+ tricks = ddTable[i, j];
+ }
+ }
+ }
+ }
+ String vulnerability = this.board.GetVulnerable().ToUpper();
+ bool vulnerable = this.determineVulnerability(vulnerability, contract.Declarer);
+ contract.Score = contract.CalculateScore(tricks, vulnerable);
+ return contract;
+ }
+
+ public ParContract GetDDTableParContract(int[,] ddTable)
+ {
+ String dealer = this.board.GetDealer();
+ String vulnerability = this.board.GetVulnerable().ToUpper();
+ ParContract nsHighest = this.getHighestMakeableContract(ddTable, true, false);
+ ParContract ewHighest = this.getHighestMakeableContract(ddTable, false, true);
+ bool nsPlaying = ("N".Equals(dealer) || "S".Equals(dealer));
+ if (nsHighest == ewHighest)
+ {
+ return nsPlaying ? nsHighest.Validate() : ewHighest.Validate();
+ }
+ ParContract highest = nsHighest.Higher(ewHighest) ? nsHighest : ewHighest;
+ ParContract otherSideHighest = nsHighest.Higher(ewHighest) ? ewHighest : nsHighest;
+ nsPlaying = ('N'.Equals(highest.Declarer) || 'S'.Equals(highest.Declarer));
+ bool defenseVulnerability = this.determineVulnerability(vulnerability, nsPlaying ? 'E' : 'N');
+ ParContract highestDefense = highest.GetDefense(ddTable, defenseVulnerability);
+ if (highestDefense != null)
+ {
+ // Highest contract has profitable defense
+ return highestDefense.Validate();
+ }
+ int denominationIndex = Array.IndexOf(BCalcWrapper.DENOMINATIONS, highest.Denomination);
+ int declarerIndex = Array.IndexOf(BCalcWrapper.PLAYERS, highest.Declarer);
+ List<int> playerIndexes = new List<int>();
+ playerIndexes.Add(declarerIndex);
+ playerIndexes.Add((declarerIndex + 2) & 3);
+ bool vulnerable = this.determineVulnerability(vulnerability, highest.Declarer);
+ int scoreSquared = highest.Score * highest.Score;
+ List<ParContract> possibleOptimums = new List<ParContract>();
+ for (int i = 0; i < 5; i++)
+ {
+ foreach (int player in playerIndexes)
+ {
+ int level = highest.Level;
+ if (i > denominationIndex)
+ {
+ level--;
+ }
+ while (level > 0)
+ {
+ ParContract contract = new ParContract(level, BCalcWrapper.DENOMINATIONS[i], BCalcWrapper.PLAYERS[player], false, 0);
+ contract.Score = contract.CalculateScore(ddTable[player, i], vulnerable);
+ if (otherSideHighest.Higher(contract))
+ {
+ // Contract is lower than other side's contract
+ break;
+ }
+ if (highest.Score * contract.Score > 0)
+ {
+ // Contract makes
+ if (Math.Abs(contract.Score) >= Math.Abs(highest.Score))
+ {
+ // Contract is profitable
+ ParContract defense = contract.GetDefense(ddTable, defenseVulnerability);
+ if (defense != null && (contract.Score * contract.Score > contract.Score * defense.Score))
+ {
+ // Contract has defense
+ possibleOptimums.Add(defense);
+ // So lower contracts will too.
+ break;
+ }
+ else
+ {
+ // Contract does not have defense
+ possibleOptimums.Add(contract);
+ }
+ }
+ else
+ {
+ // Contract is not profitable
+ break;
+ }
+ }
+ level--;
+ }
+ }
+ }
+ foreach (ParContract contract in possibleOptimums)
+ {
+ if ((Math.Abs(contract.Score) > Math.Abs(highest.Score)))
+ {
+ // Contract is more profitable
+ highest = contract;
+ }
+ else
+ {
+ if (contract.Score == highest.Score)
+ {
+ if (highest.Higher(contract))
+ {
+ // Equally profitable, but lower
+ highest = contract;
+ }
+ }
+ }
+ }
+ return highest.Validate();
+ }
+
+ public ParContract GetParContract(int[,] ddTable)
+ {
+ try
+ {
+ return this.GetJFRParContract();
+ }
+ catch (FieldNotFoundException)
+ {
+ try
+ {
+ return this.GetPBNParContract();
+ }
+ catch (FieldNotFoundException)
+ {
+ return this.GetDDTableParContract(ddTable);
+ }
+ }
+ }
+
+ }
+}