經過歐拉計劃學Rust編程(第54題)

因爲研究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。

牌面由小到大的順序是:二、三、四、五、六、七、八、九、十、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」,獲得最新的下載連接。

歷史文章:

學會10多種語言是種什麼樣的體驗?

刷完歐拉計劃中的63道基礎題,能學會Rust編程嗎?

相關文章
相關標籤/搜索