65.9K
CodeProject 正在变化。 阅读更多。
Home

编码挑战框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.38/5 (6投票s)

2015年7月10日

CPL

12分钟阅读

viewsIcon

17693

downloadIcon

253

一个用于轻松解决竞技编程网站编程挑战的框架

引言

如今网上有很多代码挑战网站。它们是学习编程、保持技能跟进(如果您已经是程序员)以及甚至赢得工作或金钱奖励的绝佳方式。它们的难度从简单到极富挑战性,并且越来越受欢迎。我喜欢接受这些挑战,但为每个挑战启动一个单独的项目很麻烦。跟踪它们并了解它们的状况很成问题,编写所有与之相关的样板代码也很乏味——尤其是当您想要一些漂亮的功能时,比如计时、编辑输入并重试,以及回看它们的网页。此外,不同的语言有不同的要求,不同的竞赛处理输入输出的方式也不同。我决定通过编写一个提供这些便利功能并将所有代码整齐地放在一起的框架来让我的生活更轻松。

背景

在这个过程中,我真正想坚持三个基本原则:

首先,我希望确保每个挑战都有一个独立的、自包含的文件。我见过许多项目设计得可以扩展,但需要修改几个分散在不同文件中的地方才能进行这样的扩展,我不想参与其中。这容易出错、乏味且通常是多余的。要在当前框架中解决一个新挑战,您只需添加一个用于该挑战的源文件。该源文件应实现 `IChallenge` 接口,该接口只需要三个函数——一个用于返回挑战的示例数据,一个用于返回该示例数据的预期输出,当然还有一个用于解决挑战的函数。实现 `IChallenge` 接口的类应使用有关所涉及的竞赛、竞赛中的挑战名称以及可选的指向挑战网站的 URL 的信息进行装饰。这就是解决挑战所需的所有内容。

其次,我希望能够代表多种语言。不同的竞赛对可以使用哪种语言有不同的要求。非托管 C++ 几乎总是一个选项,所以我肯定想要它。C# 是我个人的首选语言,所以我想要它。F# 是我一直很喜欢编程的语言,并且看起来很容易添加,所以我打算也把它加入。到目前为止,这三种语言是可用的。非托管 C++ 是一个特殊的挑战,因为 C# 和 F# 中用于装饰的属性在这里不可用。我最终使用了一些特殊的 C++ 宏来实现这一点。此外,在使用非托管 C++ 进行输入输出时,必须格外小心。

第三,我希望挑战文件至少可以选择性地直接提供给挑战网站提交。也就是说,我希望能够构建这些文件,使得最终结果可以完整地被接受作为该挑战的提交。我确实通过各种“土法炼钢”的方式实现了这一点。C++ 文件由于宏魔法而相当容易。F# 文件借助定义的常量 `CHALLENGE_RUNNER`(在 C# 和 F# 文件中都定义了)也相对容易。C# 文件稍微困难一些,但有了正确的“模板”,它们也可以很好地作为提交。

虽然文件可以格式化为可用于提交,而且这样做并不难,但这可能并不总是可取的。挑战网站可能不允许您想用以解决挑战的语言——例如,最大的挑战网站之一 UVa,只允许使用框架目前支持的三种语言中的非托管 C++ 代码。另一个问题是您可能想使用挑战网站上不可用的库。大多数网站不允许包含任意库,因此在这种情况下代码无法提交。有些网站(例如 Project Euler)甚至不接受提交。如果您能输入答案,您就解决了挑战。在这种情况下,可以通过省略提交过程的样板代码来使代码更易于处理。另外,如果您不喜欢样板代码,您可以直接写代码,然后将解决方案复制粘贴到挑战网站上。

使用代码

这是一个用于解决编程挑战的框架,因此,在您开始为其编写代码解决这些挑战之前,它本身并没有太多作用。如上所述,解决一个新挑战包括添加一个实现 `IChallenge` 接口的单个源文件。

public interface IChallenge
{
    void Solve();
    string RetrieveSampleInput();
    string RetrieveSampleOutput();
}

实现 `IChallenge` 的类应使用有关挑战的信息进行装饰。总的来说,必须包含三条关于挑战的信息。

第一个是竞赛。挑战解决方案将显示在框架中的一个树形视图中,该树形视图的节点将是您在这些装饰中为竞赛写入的任何内容。它可以是任何内容,但任何具有相同竞赛名称的解决方案都将折叠在该名称下方的树形视图中。

第二个是具体的挑战名称。同样,它可以是任何内容,但它将是在展开竞赛节点时为该挑战列出的内容。

最后,可选地,可以提供一个指向挑战页面的 URL。当在树形视图中选择挑战时,网页按钮将启用,该按钮将带您到该网页。

例如,项目中的示例代码的装饰是针对 CodeChef 的一个示例挑战。CodeChef 接受所有三种支持的语言,并且该示例挑战的所有提交都可以从 CodeChef 中查看,因此我使用了这个示例。这是示例挑战的 C# 装饰。

[Challenge("Code Chef", "Test - CS", "http://www.codechef.com/problems/TEST")]

F# 的装饰看起来也很相似。

[<Challenge("Code Chef", "Test - FS", "http://www.codechef.com/problems/TEST")>]

如前所述,C++ 没有属性,因此一个特殊的宏在 C++ 挑战中承担了这项工作。

prolog("Code Chef", Test, "Test - CPP", "http://www.codechef.com/problems/TEST")

`prolog` 宏的第二个参数是用于放置挑战的命名空间。大多数挑战网站希望将代码写在 `Main()` 中,所以 C++ 提交代码就放在那里。这当然会导致冲突,如果所有这些函数出现在同一个命名空间中,这就需要我们在 `Main()` 的每个实例周围放置一个特定于挑战的命名空间——因此,C++ 挑战中的第二个参数(命名空间)就派上用场了。

请注意,在我选择的每个挑战名称中,我都附加了 CS、FS 或 CPP,分别表示语言。这完全是我个人的约定。由于所有语言的挑战看起来都一样,如果我用多种语言解决同一个挑战,我喜欢在挑战名称中指定。可以采用不同的方式——我可以自动用不同的颜色或作为树形视图的子级别来区分,但这是目前的一种简单变通方法。

在 C# 和 F# 中,我们实现一个接口来解决新挑战。如前所述,`IChallenge` 接口有三个成员:`Solve()`、`RetrieveSampleInput()` 和 `RetrieveSampleOutput()`。对于所有三种语言,Solve(嗯,C++ 中没有“`Solve()`”——它的位置被 `Main()` 取代了,但大致意思是一样的)总是从控制台(C++ 的 stdin)获取输入,并将输出写回控制台(C++ 的 stdout)。这与大多数网站使用的协议相符——包括 UVa 和 CodeChef。对于不符合的,可能需要编写适配器。这通常很简单,生成的代码可以作为所有该竞赛挑战的样板代码。

以下是一些用于解决示例挑战的 C# 示例代码。这是最简单的版本,因此不能直接提交给网站。

using System;

namespace MiscChallenges.Challenges
{
	public static partial class ChallengeClass
	{
		[Challenge("Code Chef", "TestNS - CS", "http://www.codechef.com/problems/TEST")]
		public class ChefTestNS : IChallenge
		{
			public void Solve()
			{
				var input = Console.ReadLine();
				while (input != "42")
				{
					Console.WriteLine(input);
					input = Console.ReadLine();
				}
			}

			public string RetrieveSampleInput()
			{
				return @"
1
2
88
42
99
";
			}

			public string RetrieveSampleOutput()
			{
				return @"
1
2
88
";
			}
		}
	}
}

(可提交代码的样板代码在三个可以成功提交的示例文件中有所说明)

请注意,实现 `IChallenge` 的类是 `ChallengeClass` 的嵌套类,`ChallengeClass` 部分实现在每个挑战源文件中,并且它本身位于 `MiscChallenges.Challenges` 命名空间中。您可以将类命名为任何名称,只要它与其他 `IChallenge` 实现唯一即可。按照惯例,名称应类似于挑战名称。我使用了“@”风格的字符串来表示示例输入和输出,以便能够将输入和输出放在左侧,格式与它们需要接收时完全相同。为了做到这一点,我必须在这些字符串的起始双引号后放置一个换行符。此换行符在框架中是必需的,并在传递给挑战代码之前被移除。

F# 代码也类似。我在此包含了样板代码,因此这段代码可以原封不动地提交给 CodeChef 并通过测试……

module Test
open System

let main() =
    let mutable chk=true
    while chk do
        let x = int(Console.ReadLine())
        match x with
            | 42 -> chk<-false
            | _ -> printfn "%d" x
main()

#if CHALLENGE_RUNNER
open FS_Challenges

[<Challenge("Code Chef", "Test - FS", "http://www.codechef.com/problems/TEST")>]
type TestChallenge() = 
    interface IChallenge with
        member this.Solve() = 
            main()

        member this.RetrieveSampleInput() = @"
1
2
88
42
99
"
        member this.RetrieveSampleOutput() = @"
1
2
88
"
#endif

最后,C++ 示例代码也很简单。宏使 C++ 代码在框架和提交代码中都更容易使用,因此我将在此展示完整的可提交的 C++ 代码。

#include <iostream>
#include <queue>
#include <functional>
#include <vector>

prolog("Code Chef", Test, "Test - CPP", "http://www.codechef.com/problems/TEST")

using namespace std;
int main(void) {
	int x;
	cin >> x;
	do
	{
		cout << x << endl;
		cin >> x;
	} while (x != 42);
	return 0;
}
sampleInput
R"(
1
2
88
42
99
)";

sampleOutput
R"(
1
2
88
)";

epilog

注意这四个宏:`Prolog`、`SampleInput`、`SampleOutput` 和 `Epilog`。`Prolog` 已经讨论过,其他三个不言而喻。与 C# 一样,我在这里使用了原始字符串字面量功能,以便示例数据看起来与示例页面上的完全一样。这并非必要,但如果您不使用它,请记住这里所需的开头的换行符,它与在 C# 和 F# 文件中使用的原因相同。

差不多就是这样了。每种语言都有一个单独的项目,相应语言的挑战应明显放在正确项目中。我在“Programming Challenges”文件夹中为每个竞赛创建了单独的文件夹。我认为这不必要但很方便。文件夹结构不重要,只要所有 `IChallenge` 实现都嵌套在 `ChallengeClass` 中即可。据我所知,您无法在 C++ 或 F# 项目中创建文件夹,否则我也会在那里这样做。可能需要注意的一点是,C++ DLL 作为构建步骤使用相对路径复制到 C# 目录,这意味着这两个目录必须保持相同的相对位置,或者需要修改构建步骤。

用户界面很简单,但这里有一些需要注意的地方:

要运行挑战,只需在左侧的树形视图中选择它,然后单击运行按钮。当选择挑战时,示例输入将出现在输出窗格下方的输入窗格中。它将被传递给挑战求解器,求解器的输出将显示在上面的输出窗格中。如果它与预期输出匹配,则将显示为绿色。如果存在任何差异,则将显示为红色。

可以编辑输入以尝试新案例(如果需要)。在这种情况下,将忽略预期输出,输出将显示为黑色。如果挑战代码引发异常,异常的消息将以红色显示在输出中。由于您可以编辑输入,并且可能没有编写出非常健壮的挑战代码,这很容易发生。许多挑战对输入非常敏感,以至于很容易花费无限的时间来计算。每个挑战都在另一个线程上运行,并且在运行过程中“取消”按钮是启用的。按下“取消”按钮会立即终止线程,并在输出窗格中以红色显示“Challenge Cancelled”。对“有趣的”操作没有任何限制,因此通常避免执行任何无法被终止的操作。我知道这被认为是“危险的”,但挑战通常只做计算,所以不需要真正的保护。我曾想在装饰中添加一些内容来指示“不允许在此挑战中取消”,但还没有这样做。如果这确实是个问题,这应该很容易实现,只是会稍微增加装饰的复杂性。

最后,在求解之前会设置一个计时器,并在求解之后进行检查,以毫秒为单位显示解决挑战所需的时间。

关注点

这是一次有趣的学习经历。我以前从未尝试过用三种不同的语言在三个不同的项目中创建解决方案,因此让它们正确工作也带来了一些挑战。弄清楚如何从像 C++ 这样没有属性的语言中实现类似属性的功能也很有趣。我需要在启动时拥有来自所有 `prolog` 宏的信息,而无需手动创建一个包含所有这些信息的数组。`prolog` 宏通过将函数值分配给虚拟变量来做到这一点。相关函数将创建的 `ChallengeInfo` 对象添加到全局静态向量中,以便在 C# 程序集在启动时调用它时可用。一些巧妙的处理确保我们不会因初始化顺序问题而遇到麻烦。我从事 C++ 工作已经 15 年了,所以我不敢肯定这是最好的方法,但它似乎有效。

我希望能够轻松做到的一件事是 C# 和 F# 项目之间的循环引用。我本希望在 F# 中引用和使用 C# 属性,但那样的话就无法从 C# 项目引用 F# 项目来实际调用求解器了。后者似乎更重要,所以我只是在 F# 项目中创建了一些基本上是冗余的属性。这也不是什么大问题,因为这两个属性不一定需要相同,但如果只有一个这样的属性会更好。我想我可以创建一个只有属性的第三个项目,并从两个程序集中引用它,但这似乎有点过了。

最终,使这个项目有趣的主要原因是能够轻松地用多种语言完成大量挑战,轻松地跟踪所有挑战并查看结果。这个框架还可以轻松地用不同的语言编写解决方案并比较性能。这并不是它的主要目的,但它向我展示了一些我以前从未想过的事情。

历史

2015 年 7 月 9 日 - 首次提交

© . All rights reserved.