通过欧拉计划学Rust编程(第54题)
由于研究Libra等数字货币编程技术的需要,学习了一段时间的Rust编程,一不小心刷题上瘾。
“欧拉计划”的网址:
https://projecteuler.net
英文如果不过关,可以到中文翻译的网站:
http://pe-cn.github.io/
这个网站提供了几百道由易到难的数学问题,你可以用任何办法去解决它,当然主要还得靠编程,编程语言不限,论坛里已经有Java、C#、Python、Lisp、Haskell等各种解法,当然如果你直接用google搜索答案就没任何乐趣了。
这次解答的是第54题:
https://projecteuler.net/problem=54
题目描述:
扑克手牌
在扑克游戏中,玩家的手牌由五张牌组成,其等级由低到高分别为:
- 单牌:牌面最大的一张牌。
- 对子:两张牌面一样的牌。
- 两对:两个不同的对子。
- 三条:三张牌面一样的牌。
- 顺子:五张牌的牌面是连续的。
- 同花:五张牌是同一花色。
- 葫芦:三条带一个对子。
- 四条:四张牌面一样的牌。
- 同花顺:五张牌的牌面是连续的且为同一花色。
- 同花大顺:同一花色的10、J、Q、K、A。
牌面由小到大的顺序是:2、3、4、5、6、7、8、9、10、J、Q、K、A。
如果两名玩家的手牌处于同一等级,那么牌面较大的一方获胜;例如,一对8胜过一对5(参见例1);如果牌面相同,例如双方各有一对Q,那么就比较玩家剩余的牌中最大的牌(参见例4);如果最大的牌相同,则比较次大的牌,依此类推。
S代表黑桃(Spade),H表示红桃(Heart),D表示方块(Diamond),C表示梅花(Club),T表示10(Ten),考虑以下五局游戏中双方的手牌:
手牌 | 玩家1 | 玩家2 | 胜者 |
---|---|---|---|
1 | 5H 5C 6S 7S KD | 2C 3S 8S 8D TD | 玩家2 |
一对5 | 一对8 | ||
2 | 5D 8C 9S JS AC | 2C 5C 7D 8S QH | 玩家1 |
单牌A | 单牌Q | ||
3 | 2D 9C AS AH AC | 3D 6D 7D TD QD | 玩家2 |
三条A | 同花方片 | ||
4 | 4D 6S 9H QH QC | 3D 6D 7H QD QS | 玩家1 |
一对Q | 一对Q | ||
最大单牌9 | 最大单牌7 | ||
5 | 2H 2D 4C 4D 4S | 3C 3D 3S 9S 9D | |
葫芦 | 葫芦 | ||
(三条4) | (三条3) |
在poker.txt文本文件中,包含有两名玩家一千局的手牌。每一行包含有10张牌(均用一个空格隔开):前5张牌属于玩家1,后5张牌属于玩家2。你可以假定所有的手牌都是有效的(没有无效的字符或是重复的牌),每个玩家的手牌不一定按顺序排列,且每一局都有确定的赢家。
其中有多少局玩家1获胜?
请
先
不
要
直
接
看
答
案
,
最
好
自
己
先
尝
试
一
下
。
解题过程:
遇到一个复杂的问题,可以尝试将问题分解,变为一个个简单的情况,然后慢慢逼近最终的问题。
第一步: 先读文件,将玩家1和玩家2的牌分开。
第22题里已经学会了读文件,并且将字符串分隔成向量,再利用切片功能将前5个赋给玩家1,后5个赋给玩家2。
let data = std::fs::read_to_string("poker.txt").expect("打开文件出错"); let data2 = data.replace("\r\n", "\n"); let lines = data2.trim().split('\n'); for line in lines { let hand1 = &line[..14]; let hand2 = &line[15..]; println!("{:?} {:?}", hand1, hand2); }
第二步: 多文件管理
这个项目涉及到手牌、牌张、花色等概念,适合用面向对象的编程思路。Rust项目对多源文件的功能支持也相当不错,main.rs放主程序,poker.rs放扑克相关的模块。
一手牌Hand由多张牌Card组成,一个Card由牌点(用8位整数表示)和花色Suit构成,花色只有4种,适合用枚举表示。Rust里的枚举看上去与C/C#/Java等语言的枚举很像,但实际上它的功能远远不是一个简单的枚举。
// 文件poker.rs pub enum Suit { Spade, // 黑桃 Heart, // 红桃 Diamond, // 方块 Club, // 梅花 } pub struct Card { value: u8, // 用2到14表示2, 3, ..., 10, J, Q, K, A suit: Suit, } pub struct Hand { cards: Vec<Card>, }
main.rs需要加一行语句,告诉主程序要使用poker.rs中定义的模块。
mod poker;
这个时候,程序可以编译,会给出几个警告,提示Hand,Card和Suit这些类型从来没用过。
第三步: 构建一张牌Card
我们的任务要通过一个字符串构建出一个Card对象。比如,"8C"构建出梅花8,"TS"构建也黑桃10,"KC"为梅花K,"9H"为红桃9,"4S"为黑桃4。
这个时候要先学会Rust中的Trait概念,Trait这个东西很像Java/C#里的接口,但又不是。Rust内置不支持构造函数,下面这段代码相当于给Card定义了一个静态方法new(),相当于其它语言里的构造函数。
impl Card { pub fn new(str_card: &str) -> Card { let first_char = str_card.chars().next().unwrap(); let card_value = "..23456789TJQKA" .chars() .position(|c| c == first_char) .unwrap() as u8; let second_char = str_card.chars().nth(1).unwrap(); let card_suit = if second_char == 'S' { Suit::Spade } else if second_char == 'H' { Suit::Heart } else if second_char == 'D' { Suit::Diamond } else { Suit::Club }; Card { value: card_value, suit: card_suit, } } }
主程序里可以构造一个card(这里用梅花8),尝试打印出来。
let card = poker::Card::new("8C"); println!("{:?}", card);
编译时Rust会给出相当清楚的错误信息,还给出了修改建议
error[E0277]: `poker::Card` doesn't implement `std::fmt::Debug` --> src\main.rs:14:22 | 14 | println!("{:?}", card); | ^^^^ `poker::Card` cannot be formatted using `{:?}` | = help: the trait `std::fmt::Debug` is not implemented for `poker::Card` = note: add `#[derive(Debug)]` or manually implement `std::fmt::Debug` = note: required by `std::fmt::Debug::fmt`
我们声明了一个新类型Card,但系统并不知道如何把它转换成字符串显示出来。按照提示,我们在Card和Suit前面各加上一行语句,让系统帮我们自动实现一个输出格式。
#[derive(Debug)] pub enum Suit { Spade, // 黑桃 Heart, // 红桃 Diamond, // 方块 Club, // 梅花 } #[derive(Debug)] pub struct Card { value: u8, // 用2到14表示2, 3, ..., 10, J, Q, K, A suit: Suit, }
此时程序可以顺利编译,运行可以得到如下结果:
Card { value: 8, suit: Club }
输出得虽然有点复杂,但容易理解。如果我们就想输出"8C"这样的字符串,则需要实现Display这个trait里的fmt()函数。注意write!语句后面千万别习惯性地加个分号,否则出现的编译错误让人好困惑!
use std::fmt::{Display, Error, Formatter}; impl Display for Suit { // 只用一个字母表示: S,H,D,C fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { let name = format!("{:?}", self); write!(f, "{}", &name[..1]) } } impl Display for Card { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { let first_char = "..23456789TJQKA".chars().nth(self.value as usize).unwrap(); write!(f, "{}{}", first_char, self.suit) } }
现在构建5张牌,输出出来。
let card1 = poker::Card::new("8C"); let card2 = poker::Card::new("TS"); let card3 = poker::Card::new("KC"); let card4 = poker::Card::new("9H"); let card5 = poker::Card::new("4S"); println!("{} {} {} {} {}", card1, card2, card3, card4, card5);
第四步: 构建一手牌Hand
对于Hand,也要实现fmt()函数,还要实现一个构造函数new()。
use itertools::Itertools; impl Display for Hand { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { let str_cards = self.cards.iter().map(|x| x.to_string()).join(" "); write!(f, "{}", str_cards) } } impl Hand { pub fn new(str_cards: &str) -> Hand { let mut v = vec![]; for s in str_cards.split(' ') { v.push(Card::new(s)); } Hand { cards: v } } }
主程序这样写:
let hand = poker::Hand::new("8C TS KC 9H 4S"); println!("{}", hand);
第五步: 比较两个对象的大小
现在我们想比较两手牌的大小,主程序写成这样。
let hand1 = poker::Hand::new("8C TS KC 9H 4S"); let hand2 = poker::Hand::new("7D 2S 5D 3S AC"); if hand1 > hand2 { println!("player1 wins" ); }
想让两个对象能够相互比较大小,需要实现四个trait(Ord、PartialOrd、Eq和PartialEq)中的几个函数。
use std::cmp::{Ord, Ordering}; impl Ord for Hand { fn cmp(&self, other: &Self) -> Ordering { self.to_string().cmp(&other.to_string()) } } impl PartialOrd for Hand { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) } } impl Eq for Hand {} impl PartialEq for Hand { fn eq(&self, other: &Self) -> bool { self.to_string().eq(&other.to_string()) } }
现在,这里的比较逻辑还没有实现,暂时用字符串的比较代替。有几点要留意:
1)Ord里的cmp()函数,PartialOrd里的partial_cmp()函数,一个是表示全序,一个表示偏序。
2)cmp()和partial_cmp()两个函数的返回值有点区别,后面的多Option<>
3)Eq里的内容是空的,但必须要写
4)PartialEq里的函数名是eq()
5)实现了这些trait后,程序会自动理解“<”、“>”、“==”这些比较运算符。
第六步: 比较两手牌的大小
这时需要细心了,判断同花、顺子、四条、三条、对子等情况,为了后面的比较,我声明了一个枚举enum,用来区分各种牌型,从这里可以领略Rust里枚举的强大。
pub enum HandType { HighCard(u8, u8, u8, u8, u8), // 单牌 //对子 OnePair { value_pair: u8, max_remain: u8, // 除了对子之外,剩下最大的牌点 }, //两对 TwoPairs { high_pair: u8, // 最大的一对 low_pair: u8, // 最小的一对 max_remain: u8, // 除了两对之外,剩下最大的牌点 }, KindThree(u8), // 三条 Straight(u8), // 顺子 Flush(u8), // 同花 //葫芦,即三条带一个对子 FullHouse { value_kind_three: u8, value_pair: u8, }, KindFour(u8), // 四条 StraightFlush(u8), // 同花顺 RoyalFlush, // 同花大顺 }
再声明两个函数ranking1()和ranking2(),两次比较后能够区分大小。
impl HandType { pub fn ranking1(&self) -> u8 { match self { HighCard(_, _, _, _, _) => 0, OnePair { .. } => 1, TwoPairs { .. } => 2, KindThree(_) => 3, Straight(_) => 4, Flush(_) => 5, FullHouse { .. } => 6, KindFour(_) => 7, StraightFlush(_) => 8, RoyalFlush => 9, } } pub fn ranking2(&self) -> u64 { // ... }
这里有一个".."的语法点,忽略结构体里的内容。
FullHouse { .. } => 6,
相当于:
FullHouse { value_kind_three: _, value_pair: _, } => 6,
Ord里的cmp()和PartialEq里的eq()的逻辑也要相应修改一下。
impl Ord for HandType { fn cmp(&self, other: &Self) -> Ordering { self.ranking1() .cmp(&other.ranking1()) .then(self.ranking2().cmp(&other.ranking2())) } } impl PartialEq for HandType { fn eq(&self, other: &Self) -> bool { self.ranking1() == other.ranking1() && self.ranking2() == other.ranking2() } }
剩下就是依次判断各种情况,需要足够的耐心和测试。
use itertools::Itertools; impl Hand { //是否五张牌同一个花色。 fn is_flush(&self) -> bool { self.cards.iter().map(|card| card.suit).all_equal() } //判断五张牌是否连号,先将牌面数值从小到大排序,两两之差为1就是顺子。 fn is_straight(&self) -> bool { let mut v: Vec<u8> = self.cards.iter().map(|x| x.value).collect(); v.sort(); (0..4).all(|i| v[i + 1] - v[i] == 1) //两两之差都为1 }
第七步: 将相关类整理到一个文件夹下
源文件较多时,可以放在一个文件夹下,模块里还可以有子模块。比如这样组织文件:
src/ +---main.rs +---poker/ +---card.rs +---hand.rs +---hand_type.rs +---mod.rs +---suit.rs
poker文件夹可以自动识别为一个mod,需要mod.rs文件的配合,这里声明用到的子模块,编译器可以自动找到相应的源文件。
pub mod card; pub mod hand; pub mod hand_type; pub mod suit;
hand.rs文件里使用其它模块的内容时,需要用use语句。
use super::card::*; use super::hand_type::*;
--- END ---
我把解题的过程记录了下来,写成了一本《用欧拉计划学 Rust 编程》PDF电子书,请随意下载。
链接:https://pan.baidu.com/s/1NRfTwAcUFH-QS8jMwo6pqw
提取码:qfha
该PDF文件将来会不定期更新,可以在公众号后台回复“rust”,得到最新的下载链接。
历史文章: