Beispiel #1
0
        /// <summary>
        /// Solves the game by fictitious play and verifies the solution.
        /// </summary>
        /// <param name="snapshotAfter">Number of iterations to make an intermediate snapshot after. -1 for no intermediate snapshot.</param>
        /// <param name="configureSolver"></param>
        private StrategyTree[]  SolveAndVerifyVerifySolution(ActionTree at, ChanceTree ct, double epsilon, bool useLp)
        {
            if (useLp)
            {
                double[]       gv;
                StrategyTree[] st = EqLp.Solve(at, ct, out gv);
                VisStrategyTree.Show(st[0], Path.Combine(_outDir, "st-0.gv"));
                VisStrategyTree.Show(st[1], Path.Combine(_outDir, "st-1.gv"));
                Console.WriteLine("LP gv: {0}, {1}", gv[0], gv[1]);
            }

            FictPlayHelper fp = new FictPlayHelper
            {
                Epsilon        = epsilon,
                VisualizeTrees = true,
                BaseDir        = Path.Combine(_outDir, "fp")
            };

            StrategyTree[] eqStrategies = fp.Solve(at, ct);
            string         error;

            // Verify consistency of strategies
            for (int p = 0; p < 2; ++p)
            {
                Assert.IsTrue(VerifyAbsStrategy.Verify(eqStrategies[p], p, 1e-7, out error), string.Format("Pos {0}: {1}", p, error));
            }

            // Run VerifyEq on the computed strategies.
            Assert.IsTrue(VerifyEq.Verify(at, ct,
                                          eqStrategies, 3 * epsilon, out error), error);

            return(eqStrategies);
        }
        /// <summary>
        /// Runs FictitiousPlay with the specified parameters.
        /// Some parameters are set by default (e.g. verbosity), the caller has a chance to overwrite them
        /// using Configure delegate.
        /// </summary>
        public StrategyTree[] Solve(ActionTree at, ChanceTree ct)
        {
            int playersCount = 2;

            DirectoryExt.Delete(BaseDir);
            Directory.CreateDirectory(BaseDir);

            string inputDir = Path.Combine(BaseDir, "input");

            Directory.CreateDirectory(inputDir);
            string traceDir = Path.Combine(BaseDir, "trace");

            string chanceTreeFile = Path.Combine(inputDir, "ct.dat");
            string actionTreeFile = Path.Combine(inputDir, "at.dat");

            ct.Write(chanceTreeFile);
            at.Write(actionTreeFile);

            if (VisualizeTrees)
            {
                VisActionTree.Show(at, actionTreeFile + ".gv");
                VisChanceTree.Show(ct, chanceTreeFile + ".gv");
            }

            Solver.ChanceTreeFile     = chanceTreeFile;
            Solver.EqualCa            = false;
            Solver.ActionTreeFile     = actionTreeFile;
            Solver.OutputPath         = BaseDir;
            Solver.SnapshotsCount     = 2;
            Solver.Epsilon            = Epsilon;
            Solver.ThreadsCount       = 6;
            Solver.IsVerbose          = true;
            Solver.IterationVerbosity = 10000;
            Solver.MaxIterationCount  = 1000000000;

            if (Configure != null)
            {
                Configure(Solver);
            }
            Solver.Solve();

            StrategyTree[] eqStrategies = new StrategyTree[playersCount];

            for (int p = 0; p < playersCount; ++p)
            {
                string fileName = Solver.CurrentSnapshotInfo.StrategyFile[p];
                eqStrategies[p] = StrategyTree.Read <StrategyTree>(fileName);
                if (VisualizeTrees)
                {
                    VisStrategyTree.Show(eqStrategies[p], fileName + ".gv");
                }
            }

            return(eqStrategies);
        }
Beispiel #3
0
        private GameValue Solve(TestParams testParams, bool visualize, ConfigureSolver configureSolver)
        {
            if (visualize)
            {
                VisActionTree.Show(testParams.ActionTree,
                                   Path.Combine(_outDir, String.Format("{0}-at.gv", testParams.Name)));
                VisChanceTree.Show(testParams.ChanceTree,
                                   Path.Combine(_outDir, String.Format("{0}-ct.gv", testParams.Name)));
                for (int p = 0; p < testParams.StrategyTrees.Length; ++p)
                {
                    VisStrategyTree.Show(testParams.StrategyTrees[p],
                                         Path.Combine(_outDir, string.Format("{0}-st-{1}.gv", testParams.Name, p)));
                }
            }

            // Make sure input is correct.
            for (int p = 0; p < testParams.ChanceTree.Nodes[0].Position; ++p)
            {
                string errorText;
                Assert.IsTrue(VerifyAbsStrategy.Verify(testParams.StrategyTrees[p], p, 0.000001, out errorText), errorText);
            }

            GameValue gv = new GameValue {
                ChanceTree = testParams.ChanceTree, ActionTree = testParams.ActionTree, Strategies = testParams.StrategyTrees
            };

            gv.PrepareVis = visualize;

            if (configureSolver != null)
            {
                configureSolver(gv);
            }

            gv.Solve();

            if (visualize)
            {
                for (int p = 0; p < testParams.ChanceTree.PlayersCount; ++p)
                {
                    GameValue.Vis.Show(gv, p, Path.Combine(_outDir, String.Format("{0}-{1}-val.gv", testParams.Name, p)));
                }
            }

            Assert.AreEqual(2, gv.Values.Length);
            for (int p = 0; p < testParams.ChanceTree.PlayersCount; ++p)
            {
                Console.WriteLine("Game value pos {0}: {1}", p, gv.Values[p]);
                Assert.AreEqual(testParams.ExpectedResult[p], gv.Values[p], testParams.Epsilon);
            }
            return(gv);
        }
        private void ShowTree(GameDefinition gd)
        {
            for (int pos = 0; pos < gd.MinPlayers; ++pos)
            {
                StrategyTree st = TreeHelper.CreateStrategyTree(gd, pos);

                string fileName = string.Format("{0}-{1}.gv", gd.Name, pos);
                using (TextWriter w = new StreamWriter(File.Open(Path.Combine(_outDir, fileName), FileMode.Create)))
                {
                    VisStrategyTree vis = new VisStrategyTree {
                        Output = w, CardNames = gd.DeckDescr.CardNames
                    };
                    vis.Show(st, 3);
                }
            }
        }
Beispiel #5
0
        private void Solve(TestParams testParams, bool visualize, ConfigureSolver configureSolver)
        {
            if (visualize)
            {
                VisActionTree.Show(testParams.ActionTree,
                                   Path.Combine(_outDir, String.Format("{0}-at.gv", testParams.Name)));
                VisChanceTree.Show(testParams.ChanceTree,
                                   Path.Combine(_outDir, String.Format("{0}-ct.gv", testParams.Name)));
            }

            StrategyTree [] eqStrategies = new StrategyTree[testParams.ChanceTree.PlayersCount];

            string error;

            for (int heroPos = 0; heroPos < testParams.ChanceTree.PlayersCount; ++heroPos)
            {
                // Create and configure EqLp solver
                EqLp solver = new EqLp
                {
                    HeroPosition = heroPos,
                    ChanceTree   = testParams.ChanceTree,
                    ActionTree   = testParams.ActionTree,
                };

                if (configureSolver != null)
                {
                    configureSolver(solver);
                }

                // Solve EqLp
                solver.Solve();
                eqStrategies[heroPos] = solver.Strategy;

                if (visualize)
                {
                    VisStrategyTree.Show(solver.Strategy,
                                         Path.Combine(_outDir, string.Format("{0}-eq-{1}.gv", testParams.Name, heroPos)));
                }

                // Verify the eq value and strategy
                Assert.AreEqual(testParams.ExpectedResult[heroPos], solver.Value, testParams.Epsilon, "Wrong eq value");
                Assert.IsTrue(VerifyAbsStrategy.Verify(solver.Strategy, solver.HeroPosition, 1e-7, out error), error);
            }
            // Verify eq, use another (better) epsilon because EqLp and VerifyEq have better precision
            // than most of the reference game solvers like OCFR.
            Assert.IsTrue(VerifyEq.Verify(testParams.ActionTree, testParams.ChanceTree, eqStrategies, 1e-7, out error), error);
        }
Beispiel #6
0
        public void Test_Kuhn()
        {
            GameDefinition gd = XmlSerializerExt.Deserialize <GameDefinition>(
                Props.Global.Expand("${bds.DataDir}ai.pkr.metastrategy/kuhn.gamedef.xml"));

            ChanceTree ct = CreateChanceTreeByGameDef.Create(gd);
            ActionTree at = CreateActionTreeByGameDef.Create(gd);

            StrategyTree [] playerTrees = new StrategyTree[2];
            for (int p = 0; p < 2; ++p)
            {
                ChanceTree   pct = ExtractPlayerChanceTree.ExtractS(ct, p);
                StrategyTree st  = CreateStrategyTreeByChanceAndActionTrees.CreateS(pct, at);
                playerTrees[p] = st;

                VisStrategyTree.Show(st, Path.Combine(_outDir, string.Format("pt-{0}.gv", p)));
            }
        }
        public void Test_Leduc()
        {
            GameDefinition gd = XmlSerializerExt.Deserialize <GameDefinition>(
                Props.Global.Expand("${bds.DataDir}ai.pkr.metastrategy/leduc-he.gamedef.xml"));

            for (_heroPos = 0; _heroPos < gd.MinPlayers; ++_heroPos)
            {
                StrategyTree st = CreateValidStrategy(gd);

                string fileName = Path.Combine(_outDir, string.Format("{0}-{1}.gv", gd.Name, _heroPos));
                VisStrategyTree.Show(st, fileName);

                string errorText;
                Assert.IsTrue(VerifyCondStrategy.Verify(st, _heroPos, out errorText));

                // Now make some errors.
                if (_heroPos == 0)
                {
                    st.Nodes[339].Probab += 0.1;
                    Assert.IsFalse(VerifyCondStrategy.Verify(st, _heroPos, out errorText));
                    string expTextBegin = string.Format("Node {0}:", 291);
                    Assert.AreEqual(expTextBegin, errorText.Substring(0, expTextBegin.Length));
                    st.Nodes[339].Probab -= 0.1;

                    st.Nodes[348].Probab += 0.1;
                    Assert.IsFalse(VerifyCondStrategy.Verify(st, _heroPos, out errorText));
                    expTextBegin = string.Format("Node {0}:", 345);
                    Assert.AreEqual(expTextBegin, errorText.Substring(0, expTextBegin.Length));
                }
                else
                {
                    st.Nodes[435].Probab += 0.1;
                    Assert.IsFalse(VerifyCondStrategy.Verify(st, _heroPos, out errorText));
                    string expTextBegin = string.Format("Node {0}:", 387);
                    Assert.AreEqual(expTextBegin, errorText.Substring(0, expTextBegin.Length));
                    st.Nodes[435].Probab -= 0.1;

                    st.Nodes[432].Probab += 0.1;
                    Assert.IsFalse(VerifyCondStrategy.Verify(st, _heroPos, out errorText));
                    expTextBegin = string.Format("Node {0}:", 429);
                    Assert.AreEqual(expTextBegin, errorText.Substring(0, expTextBegin.Length));
                }
            }
        }
        public void Test_AnalyzeS()
        {
            GameDefinition gd = XmlSerializerExt.Deserialize <GameDefinition>(
                Props.Global.Expand("${bds.DataDir}ai.pkr.metastrategy/kuhn.gamedef.xml"));

            string[] strategyFiles = new string[] { "eq-KunhPoker-0-s.xml", "eq-KunhPoker-1-s.xml" };
            for (int pos = 0; pos < 2; ++pos)
            {
                string       strFile = Path.Combine(_testResDir, strategyFiles[pos]);
                StrategyTree st      = XmlToStrategyTree.Convert(strFile, gd.DeckDescr);
                VisStrategyTree.Show(st, Path.Combine(_outDir, string.Format("{0}-{1}.gv", gd.Name, pos)));
                AnalyzeStrategyTree an = new AnalyzeStrategyTree
                {
                    StrategyTree = st,
                    IsAbsolute   = true,
                    HeroPosition = pos,
                    IsVerbose    = true
                };
                an.Analyze();
                Assert.AreEqual(15, an.LeavesCount);
                if (pos == 0)
                {
                    Assert.AreEqual(12, an.MovesCount);
                    Assert.AreEqual(5, an.ZaspMovesCount);
                    Assert.AreEqual(3, an.ZaspLeavesCount);
                    Assert.AreEqual(1, an.Statistics.Count);
                    Assert.AreEqual(5, an.Statistics[0].NZaspMovesCount);
                    Assert.AreEqual(1 + 0.33333, an.Statistics[0].SumNZaspFold, 0.00001);
                    Assert.AreEqual(0.66667 + 1 + 0.66667, an.Statistics[0].SumNZaspCall, 0.00001);
                    Assert.AreEqual(0.33333 + 1, an.Statistics[0].SumNZaspRaise, 0.00001);
                }
                else
                {
                    Assert.AreEqual(12, an.MovesCount);
                    Assert.AreEqual(4, an.ZaspMovesCount);
                    Assert.AreEqual(3, an.ZaspLeavesCount);
                    Assert.AreEqual(1, an.Statistics.Count);
                    Assert.AreEqual(6, an.Statistics[0].NZaspMovesCount);
                    Assert.AreEqual(1 + 0.66667, an.Statistics[0].SumNZaspFold, 0.00001);
                    Assert.AreEqual(0.66667 + 1 + 0.33333 + 1, an.Statistics[0].SumNZaspCall, 0.00001);
                    Assert.AreEqual(0.33333 + 1, an.Statistics[0].SumNZaspRaise, 0.00001);
                }
            }
        }
Beispiel #9
0
        public void Test_Eq()
        {
            GameDefinition gd = XmlSerializerExt.Deserialize <GameDefinition>("kuhn8.gamedef.xml");
            ChanceTree     ct = CreateChanceTreeByGameDef.Create(gd);

            VisChanceTree.Show(ct, "kuhn8-ct.gv");
            ActionTree at = CreateActionTreeByGameDef.Create(gd);

            VisActionTree.Show(at, "kuhn8-at.gv");



            double[]        values;
            StrategyTree [] strategies = EqLp.Solve(at, ct, out values);
            Console.WriteLine("Kuhn8 eq values: {0}, {1}", values[0], values[1]);
            VisStrategyTree.Show(strategies[0], "kuhn8-eq-0.gv");
            VisStrategyTree.Show(strategies[1], "kuhn8-eq-1.gv");

            // Make strategy for T same as for Q
            //strategies[0].Nodes[strategies[0].FindNode("0d0 0p0", null)].Probab = 0.5;
            //strategies[0].Nodes[strategies[0].FindNode("0d0 0p0 1p1 0p1", null)].Probab = 0.5;
            //strategies[0].Nodes[strategies[0].FindNode("0d0 0p1", null)].Probab = 0.5;


            // Make strategy for Q same as for T
            strategies[0].Nodes[strategies[0].FindNode("0d2 0p0", null)].Probab         = 0;
            strategies[0].Nodes[strategies[0].FindNode("0d2 0p0 1p1 0p1", null)].Probab = 0;
            strategies[0].Nodes[strategies[0].FindNode("0d2 0p1", null)].Probab         = 1;

            VisStrategyTree.Show(strategies[0], "kuhn8-eq-0-adj.gv");


            Br br = new Br {
                ActionTree = at, ChanceTree = ct, HeroPosition = 1
            };

            br.Strategies = new StrategyTree[] { strategies[0], null };
            br.Solve();
            StrategyTree br0 = br.Strategies[1];

            Console.WriteLine("Br against pos 0: {0}", br.Value);
            VisStrategyTree.Show(strategies[1], "kuhn8-eq-br-0.gv");
        }
        public void Test_CallKK()
        {
            string outDir = Path.Combine(_outDir, "call-KK");

            Directory.CreateDirectory(outDir);
            HePocketKind[] sbPockets = new HePocketKind[] { HePocketKind._AA, HePocketKind._KK, HePocketKind._AKs };
            HePocketKind[] bbPockets = new HePocketKind[] { HePocketKind._AA, HePocketKind._KK, HePocketKind._QQ, HePocketKind._AKs };

            string     xmlAt = Props.Global.Expand("${bds.DataDir}ai.pkr.holdem.learn/bigcards-pf-1.xml");
            ActionTree at    = XmlToActionTree.Convert(xmlAt);

            VisActionTree.Show(at, Path.Combine(outDir, "at.gv"));

            BigCardsPreflop bc = new BigCardsPreflop();

            bc.Solve(at, sbPockets, bbPockets);

            Console.WriteLine("Game values: {0}, {1}", bc.GameValues[0], bc.GameValues[1]);
            VisChanceTree.Show(bc.Ct, Path.Combine(outDir, "ct.gv"));
            VisStrategyTree.Show(bc.Strategies[0], Path.Combine(outDir, "st-0.gv"));
            VisStrategyTree.Show(bc.Strategies[1], Path.Combine(outDir, "st-1.gv"));
        }
Beispiel #11
0
        /// <summary>
        /// Runs the solver with the specified parameters..
        /// </summary>
        /// <param name="iterCounts">Number of iterations for each run, -1 - unlimited.</param>
        private StrategyTree RunSolver(TestParams testParams, bool visualize, bool trace, int [] iterCounts, ConfigureSolver configureSolver)
        {
            int playersCount = testParams.ChanceTree.PlayersCount;

            string baseDir = Path.Combine(_outDir, testParams.Name);

            DirectoryExt.Delete(baseDir);
            Directory.CreateDirectory(baseDir);

            string inputDir = Path.Combine(baseDir, "input");

            Directory.CreateDirectory(inputDir);
            string traceDir = Path.Combine(baseDir, "trace");

            if (trace)
            {
                Directory.CreateDirectory(traceDir);
            }

            string chanceTreeFile = Path.Combine(inputDir, "ct.dat");
            string actionTreeFile = Path.Combine(inputDir, "at.dat");

            testParams.ChanceTree.Write(chanceTreeFile);
            testParams.ActionTree.Write(actionTreeFile);

            if (visualize)
            {
                VisActionTree.Show(testParams.ActionTree, actionTreeFile + ".gv");
                VisChanceTree.Show(testParams.ChanceTree, chanceTreeFile + ".gv");
            }


            int  runs   = iterCounts.Length;
            Breb solver = null;

            for (int r = 0; r < runs; ++r)
            {
                // Create and configure a solver
                solver = new Breb
                {
                    GameDef        = testParams.GameDef,
                    ActionTreeFile = actionTreeFile,
                    ChanceTreeFile = chanceTreeFile,
                    OutputPath     = baseDir,
                    SnapshotsCount = 2,
                    Epsilon        = testParams.Epsilon,
                    ThreadsCount   = DEFAULT_THREADS_COUNT,
                };
                if (trace)
                {
                    solver.TraceDir = traceDir;
                }
                solver.MaxIterationCount = iterCounts[r];
                if (configureSolver != null)
                {
                    configureSolver(solver);
                }
                solver.Solve();
            }

            string       fileName   = solver.CurrentSnapshotInfo.StrategyFile;
            StrategyTree eqStrategy = StrategyTree.Read <StrategyTree>(fileName);

            if (visualize)
            {
                VisStrategyTree.Show(eqStrategy, fileName + ".gv");
            }
            return(eqStrategy);
        }
Beispiel #12
0
        private static bool ProcessStrategyTree()
        {
            StrategyTree tree;

            if (_cmdLine.Input != "")
            {
                if (_inputFormat == ".dat")
                {
                    tree = UFTree.Read <StrategyTree>(_cmdLine.Input);
                }
                else if (_inputFormat == ".txt")
                {
                    tree = DumpStrategyTree.FromTxt(_cmdLine.Input);
                }
                else
                {
                    Console.Error.WriteLine("Unsupported input format '{0}' for tree kind '{1}'", _inputFormat, _cmdLine.TreeKind);
                    return(false);
                }
            }
            else
            {
                ActionTree at  = CreateActionTreeByGameDef.Create(_gd);
                ChanceTree ct  = CreateChanceTreeByGameDef.Create(_gd);
                ChanceTree pct = ExtractPlayerChanceTree.ExtractS(ct, _cmdLine.Position);
                tree = CreateStrategyTreeByChanceAndActionTrees.CreateS(pct, at);
            }

            if (_outputFormat == ".gv")
            {
                using (TextWriter w = new StreamWriter(File.Open(_cmdLine.Output, FileMode.Create)))
                {
                    VisStrategyTree vis = new VisStrategyTree {
                        Output = w
                    };
                    if (_gd != null)
                    {
                        vis.CardNames = _gd.DeckDescr.CardNames;
                    }
                    if (_cmdLine.ClearExpr)
                    {
                        vis.ShowExpr.Clear();
                    }
                    vis.ShowExprFromString(_cmdLine.ShowExpr);
                    vis.PruneIfExt = (t, n, s, d) => s[d].Round > _cmdLine.MaxRound;
                    vis.MatchPath  = _cmdLine.MatchPath;
                    vis.Show(tree, _cmdLine.Root);
                }
            }
            else if (_outputFormat == ".xml")
            {
                using (TextWriter w = new StreamWriter(File.Open(_cmdLine.Output, FileMode.Create)))
                {
                    XmlWriterSettings settings = new XmlWriterSettings();
                    settings.Encoding = Encoding.ASCII;
                    settings.Indent   = true;
                    using (XmlWriter xmlWriter = XmlWriter.Create(w, settings))
                    {
                        StrategyTreeToXml xmlizer = new StrategyTreeToXml {
                            Output = xmlWriter
                        };
                        if (_cmdLine.ClearExpr)
                        {
                            xmlizer.ShowExpr.Clear();
                        }
                        xmlizer.ShowExprFromString(_cmdLine.ShowExpr);
                        xmlizer.Convert(tree);
                    }
                }
            }
            else if (_outputFormat == ".dat")
            {
                tree.Write(_cmdLine.Output);
            }
            else if (_outputFormat == ".txt")
            {
                DumpStrategyTree.ToTxt(tree, _cmdLine.Output);
            }
            else
            {
                Console.Error.WriteLine("Unsupported ouput format '{0}' for tree kind '{1}'", _outputFormat, _cmdLine.TreeKind);
                return(false);
            }
            return(true);
        }
Beispiel #13
0
        private void Solve(TestParams testParams, bool visualize, ConfigureSolver configureSolver)
        {
            if (visualize)
            {
                VisActionTree.Show(testParams.ActionTree,
                                   Path.Combine(_outDir, String.Format("{0}-at.gv", testParams.Name)));
                VisChanceTree.Show(testParams.ChanceTree,
                                   Path.Combine(_outDir, String.Format("{0}-ct.gv", testParams.Name)));
                for (int p = 0; p < testParams.StrategyTrees.Length; ++p)
                {
                    VisStrategyTree.Show(testParams.StrategyTrees[p],
                                         Path.Combine(_outDir, string.Format("{0}-st-{1}.gv", testParams.Name, p)));
                }
            }

            // Make sure input is correct.
            for (int p = 0; p < testParams.ChanceTree.Nodes[0].Position; ++p)
            {
                string errorText;
                Assert.IsTrue(VerifyAbsStrategy.Verify(testParams.StrategyTrees[p], p, 0.000001, out errorText), errorText);
            }

            for (int heroPos = 0; heroPos < testParams.ChanceTree.PlayersCount; ++heroPos)
            {
                // Create and configure Br solver
                Br br = new Br
                {
                    HeroPosition = heroPos,
                    ChanceTree   = testParams.ChanceTree,
                    ActionTree   = testParams.ActionTree,
                };
                br.Strategies = new StrategyTree[testParams.ChanceTree.PlayersCount];
                for (int opp = 0; opp < testParams.ChanceTree.PlayersCount; ++opp)
                {
                    if (opp == heroPos)
                    {
                        continue;
                    }
                    br.Strategies[opp] = testParams.StrategyTrees[opp];
                }
                br.PrepareVis = visualize;

                if (configureSolver != null)
                {
                    configureSolver(br);
                }

                // Solve Br
                br.Solve();

                if (visualize)
                {
                    Br.Vis.Show(br, Path.Combine(_outDir, String.Format("{0}-br-{1}.gv", testParams.Name, heroPos)));
                }

                // Verify the Br value and strategy

                Assert.AreEqual(testParams.ExpectedResult[heroPos], br.Value, testParams.Epsilon, "Wrong BR value");

                string error;
                Assert.IsTrue(VerifyAbsStrategy.Verify(br.Strategies[br.HeroPosition], br.HeroPosition, out error),
                              error);

                // Verify Br strategy has the expected value by running an independent GameValue algo on it.
                GameValue gv = new GameValue
                {
                    ActionTree = br.ActionTree,
                    ChanceTree = br.ChanceTree,
                    Strategies = br.Strategies
                };
                gv.Solve();
                Assert.AreEqual(testParams.ExpectedResult[heroPos], gv.Values[br.HeroPosition], testParams.Epsilon, "Wrong GameValue value");
            }
        }