因爲研究Libra等數字貨幣編程技術的須要,學習了一段時間的Rust編程,一不當心刷題上癮。git
刷完歐拉計劃中的63道基礎題,能學會Rust編程嗎?github
「歐拉計劃」的網址:
https://projecteuler.net編程
英文若是不過關,能夠到中文翻譯的網站:
http://pe-cn.github.io/編程語言
這個網站提供了幾百道由易到難的數學問題,你能夠用任何辦法去解決它,固然主要還得靠編程,編程語言不限,論壇裏已經有Java、C#、Python、Lisp、Haskell等各類解法,固然若是你直接用google搜索答案就沒任何樂趣了。函數
此次解答的是第54題:
https://projecteuler.net/problem=54學習
題目描述:測試
撲克手牌網站
在撲克遊戲中,玩家的手牌由五張牌組成,其等級由低到高分別爲:ui
牌面由小到大的順序是:二、三、四、五、六、七、八、九、十、J、Q、K、A。google
若是兩名玩家的手牌處於同一等級,那麼牌面較大的一方獲勝;例如,一對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」,獲得最新的下載連接。
歷史文章: