【Web】徹底理解同源政策(Same Origin Policy)

對於網頁學習者來說,同源政策(Same Origin Policy)可能是一個很重要的困惑點。

當我們終於搞懂了client與server;理解了HTTP request/response;摸熟了AJAX;想出了一個絕佳的專案點子⋯⋯但不知為何,API的資料就是讀不出來。

於是我們開始研究關鍵字「同源政策」並持續Google,每次都好像又多懂了一點,但又不是那麼肯定——特別是在部署至production環境時,只要網域轉換,似乎就會發生不可預期的效果。

讓我們試著一次把它搞懂吧。

Meme of AJAX 圖片:Ajax迷因。Same Origin Policy是件麻煩的事(出處:reddit)

瀏覽器/使用者代理人/安全性

要理解同源政策,最核心的重點就是理解:

同源政策是由「誰」來執行?以及「為什麼」?

是HTTP(S)的嗎?還是JavaScript?是瀏覽器嗎?是網站框架嗎?還是TCP/IP?

如果能將以下重點銘記在心,我想在閱讀文件時就會感到豁然開朗:

同源政策,Same Origin Policy,是「瀏覽器」作為使用者代理人,為了「安全性考量」,所設定的規範。

什麼是使用者代理人?

請試著想像如下情境:我們拿著雙證件到銀行開了戶,也存了錢進去。行員告知:如果需要匯款或是其他操作,可以透過帳號+密碼登入銀行網站,在瀏覽器中進行——這樣就不用每次都跑銀行囉。

在現實世界裡,與我們互動的是銀行行員,我們可以透過證件與存摺存取帳戶;但在網路銀行中,與我們互動的對象成了Web server,而驗證身份的方式則變成帳號+密碼。

當我們請銀行行員協助時,使用的是人類的自然語言。但在網路銀行中,我們與web server是透過使用者介面/通訊協定/網路訊號來進行溝通的。

但作為一個人類,我們無法用大腦解析這一切。

於是我們需要一個代理人(user agent)——也就是瀏覽器,協助我們與web server進行溝通。這包含了渲染UI、設定Cookie、執行TCP三向交握、通訊加密、並將我們的表單解析成Http Request後送出⋯⋯等。

使用者的「安全性」是瀏覽器的職責

在操作的過程裡,瀏覽器有義務保護使用者的資料——尤其是涉及身份驗證的部分。

我們會透過瀏覽器登入各種帳號(例如網路銀行、臉書、Gmail、以及各式各樣來路不明的網站)。當成功使用帳號密碼登入時,這些服務的Web server會透過HTTP header, 提供一段auth token並要求瀏覽器存在cookie裡。在auth token的有效期間內,我們的http request都會自動帶上這段cookie,以此為憑,我們因此可以對服務進行各種操作,例如匯款、貼文、按讚、回覆email⋯⋯等。

但問題來了,如果有一個惡意的盜版漫畫網站evil.com,當我們在訪問時,透過頁面中JS以及AJAX,偷偷在瀏覽器中執行這樣的行為呢:

「請把銀行bank.com的cookie資料(包含auth token)讀取出來;透過AJAX+POST;回傳到https://evil.com/sniff

當然,作為一個使用者代理人,瀏覽器會拒絕如是請求。

拒絕的原則是什麼呢?那就是同源政策最重要的一環:「非同源的資料讀取是不被允許的」。既然evil.com不能讀取bank.com設定的cookie,那當然也無法將之回傳。

火狐姬

同源(Same-origin)的定義

如果能充分了解以上原則。剩下的,就只是技術細節的問題。

我們可以透過火狐家的MDN文件理解同源政策的規範。一言以敝之,我們可以把origin視做一個three-tuple(scheme, host, port),也就是(通訊協定, 網域, 埠號)的組合。兩個document的origin只要任一欄位不相同,就會被瀏覽器視作不同源[1]。

舉例如下:

此外,同源政策是以document為單位的。舉例來說:假設store.mystie.com的document透過<script>標籤取得了來自cdn.google.com的JS腳本,則該腳本的origin仍是由blog.mystie.com來定義。

在實務中,我們最常遇到的origin變數往往是網域。為了方便起見,以下討論會將網域等同於origin使用,但請注意origin包含的不只有網域而已。

SOP show domain 圖片:我們可以透過document.domain來取得文件所屬的網域。

跨源存取細節

討論到這裡,可能我們已經想到一堆例外了⋯⋯

  • 我的網站會透過CDN導入jQuery函式庫和Bootstrap的CSS,為什麼這不受限制?
  • 我的網站會使用<img>讀取外站圖片,為什麼這不受限制?

事實上,同源政策其實有許多細節,甚至各家瀏覽器的實作都會有些微不同。請注意,當我們在討論這個問題時,我們的角色已經轉變成服務的提供者與Web Server了。在研究同源政策時,釐清主詞(使用者/瀏覽器/網站後端)以及行為(request/response)是最重要的起手。

我們可以透過MDN的文件理解不同的跨源行為

跨源寫入(write)原則上允許

可以在A網域的document中提交請求給B網站;也可以透過超連結、轉址等方式連結至B網站。

跨源嵌入(embed)原則上允許

可以使用<img> <video> <iframe>標籤嵌入來自其他網域的資源。

可以使用<link rel="stylesheet" href="…">套用來自其他網域的CSS。

可以使用<scrip src="">標籤嵌入來自其他網域的腳本[3]

跨源讀取(read)原則上禁止

A網域的document不能讀取B網域的本地端儲存資料(例如Cookie)與回應。

值得注意的是:我們可以在A網域的document中,使用JS發送AJAX Request給B(跨源寫入允許);但就算B有回應;文件也無法讀取response(跨源讀取禁止)。例如這樣的jQuery操作$.get("https://www.google.com ")在所有非Google的document中,都會回報disallows reading的錯誤。

那第三方的Client端API是怎麼實作的?

如果跨源讀取是被禁止的,作為一個開發者(假設是交友網站),如果我們希望有一個第三方API服務(例如臉書),可以讓我們透過瀏覽器+AJAX取得資料,這其中的實作原理是什麼呢?

實情是:臉書作為第三方服務的提供者(務必注意主詞!),在回應請求時,是可以透過HTTP header中的Access-Control-Allow-Origin欄位,請瀏覽器特別通融。

舉例來說,當我們的交友網站向臉書申請API帳號時,臉書會提供一組token並詢問我方網域(假設是https://friend.com)。當我們使用這組token請求資料時,臉書就會在response中設定Access-Control-Allow-Origin=https://friend.com,翻成白話就是:

雖然 friend.com 與我不同源,但請准許他讀取這則HTTP response。

當收到這樣的回應時,瀏覽器就會允許friend.com之下的document讀取來自facebook API的回應囉[4]。

如果API server將Access-Control-Allow-Origin設定為*時,代表任何origin都可以讀取回覆,不受同源政策限制。

當然,回到一開始銀行的例子。基於安全性考量(只賦予必要的最小權限),當然就沒有必要設定這個欄位了。

client_app 圖片:使用client端API時,設定網域通常是必要的

那麼⋯⋯我們就這樣高枕無憂了嗎?

與同源政策常常一起討論,且觀念容易糾纏在一起的另一個主題,是所謂的跨域請求偽造攻擊(Cross Site Request Forgery)。這兩者彼此概念交錯,但其實同源政策並不能完全防止跨域請求偽造攻擊——不過這也是另一個主題了,我們就有空再說吧。

目前為止,希望我們已經清楚解釋了同源政策的大概念。


註解

[1] 在RFC-6454原文中的host對應的中文應該是「主機名稱」。但本篇文章主要聚焦於瀏覽器的一般用例(而非各種形式的user agent),因此使用「網域」一詞雖然不完全精準,但為了可讀性仍然選用。

[2] cookie的讀取權限可以下放給子網域。細節請見RFC-6256中的Domain Matching章節。

[3] 請注意這包含了「伺服器端後果自負」的前提。作為後端服務的開發者,在document中嵌入來自第三方(諸如Google CDN)的JavaScript意即「允許第三方腳本以我的origin身份進行操作」——這是另一個網頁安全性的大主題,有興趣的讀者可以搜尋Cross Site Scripting(XSS)。

[4] 請注意這只是允許讀取response,並不包含允許friend.com讀取facebook.com的cookie的意味。

[ref] IETF | RFC-6265 HTTP State Management Mechanism

[ref] IETF | RFC-6454 The Web Origin Concept

[ref] LiveOverflow | CSRF Introduction and what is the Same-Origin Policy? Concept

[ref] MDN | Same Origin Policy

[ref] stackoverflow | Does including all these 3rd party javascript files impose a security risk?

【程式】使用JavaScript在中文與英文間墊上空格

中文文字工作者應該都聽過這個排版準則:

中英文交界處必須增加一個空格,避免視覺上過於壅擠[1]。

在部分的軟體或系統中,會自動墊上這樣的空間(包含你現在正看的這篇文章,以及諸如蘋果與Google的作業系統[2][3]);然而,並不是所有的讀寫介面都內建有這樣的功能。根據筆者實測,在2020年3月,Facebook的iPhone應用會自動墊上這個空間;但是在Web中則沒有——前者亦不確定是來自iPhone的作業系統,抑或是APP本身的功能。

中英文交錯的排版圖片:上圖中中英文沒有空間,顯得擁擠;下圖中則添加了半行空格。你覺得有差嗎⋯⋯可以確定的是,有人覺得有差。

所以⋯⋯我們要手動補上空白嗎

儘管這個空白是很常見的需求。然而,就筆者的觀點來看。在文本原始檔中「手動」添加空格,並不是一個理想的做法。其原因如下:

  • 非常容易疏漏。
  • 不同字體可能有不同適合的空間。
  • 在文本的原始碼中加入非必要的空格會讓管理複雜化,增加修訂、編輯與校對的負擔。
  • 以上,如果文本需要長期維護與修改,則花費的精力與效果不成比例。

讓程式來做吧!

但如果我們還是希望達成最舒適的排版,要怎麼做呢?

個人覺得最好的作法就是在閱讀的輸出端,利用程式自動墊上這個空間。

以本篇部落格為例,當訪客在讀取頁面時,筆者啟動了一段JavaScript,會將頁面中部分區塊的內容進行自動化處理。也因此,部落格中所有的文章,中英文間都會自動墊上一個半形空格。

該功能的實作如下:

function getPaddedCnEnString(htmlStr, padding) {
  // ASCII 33~126
  const EN_ALPHA = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
  const EN_SET = new Set(EN_ALPHA.split(""));
  const CN_MARKS = "。,、;:「」『』()—-?!——⋯⋯〈〉・《》_~";
  const CN_MARKS_SET = new Set(CN_MARKS.split(""));
  const CN_RANGE_MIN = 0x3400;
  const CN_RANGE_MAX = 0x9FFC;
  // utility function
  const isCn = (char) => ((char.charCodeAt(0) >= CN_RANGE_MIN && char.charCodeAt(0) <= CN_RANGE_MAX) ||
    CN_MARKS_SET.has(char));
  const isEn = (char) => EN_SET.has(char);
  // process
  let result = "";
  let thisIdx = 0;
  while (thisIdx <= htmlStr.length) {
    const thisChar = htmlStr.charAt(thisIdx);
    let nextIdx = thisIdx + 1;
    let nextChar = htmlStr.charAt(nextIdx);
    // copy contents in html tags (do no processing)
    if (thisChar === "<") {
      while (nextIdx <= htmlStr.length && htmlStr[nextIdx - 1] !== ">") {
        nextIdx++;
      }
      result += htmlStr.substring(thisIdx, nextIdx);
      thisIdx = nextIdx;
      continue
    } else if (nextChar === "<") {
      while (nextIdx <= htmlStr.length && htmlStr[nextIdx - 1] !== ">") {
        nextIdx++;
      }
    }
    result += htmlStr.substring(thisIdx, nextIdx);
    nextChar = htmlStr.charAt(nextIdx);
    // compare with next char and pad space if necessary
    if (isCn(thisChar) && isEn(nextChar)) { // 中文_abc
      result += padding;
    } else if (isEn(thisChar) && isCn(nextChar)) { // abc_中文
      result += padding;
    }
    // continue
    thisIdx = nextIdx;
  }
  return result;
}
  • 函式的輸入為:
    • htmlStr:字串,可包含HTML標籤(HTML標籤中的屬性不會被處理)
    • padding:你可以使用半形空白" "或是例如<span>標籤,再透過CSS來調整)
  • 函式使用Unicode判斷字元屬於中文還是英文,並將台灣官方的標準標點符號視做中文[4]。以此為判斷,自動在中英文間加上空間。

舉例來說,如果你的文章位於<div id="post">的標籤裡,那麼你可以這麼做[5]

$(document.onload).ready(function () {
  const element = $("#post");
  const newHTML = getPaddedCnEnString(element.html()," ");
  element.html(newHTML);
})

根據筆者簡單的測試,一份原始碼長5000字左右的markdown文章,以其HTML格式進行轉換,可以控制在5毫秒以內;且不妨礙原本內容的讀取;顯然不會造成太大的效能負擔。

以上,獻給排版的偏執狂們。


註解

[1] 儘管就筆者所知,這樣的準則並沒有被明文記載在各大軟體廠的設計文件裡。此外,筆者也反對將這樣的標準用於判斷寫作者是否專業;亦不建議用手動處理這個程序:"The space bar is not a design tool"。

[2] 出處:知乎|https://www.zhihu.com/question/19587406

[3] 非正規的指南例如:Tunghsiao Liu|中文文案排版指北|GitHub。這份文件在2020年3月時累計了7500個GitHub星星。

[4] 在Liu的指南中主張中文標點與英文間不需額外墊上空格。這其實很主觀,但我們的函示仍然提供了設定的參數。

[5] 文中預設使用jQuery在前端執行轉換。如果你可以控制後端的原始碼,也可以將轉換放在伺服器端進行。另外,如果放在前端且你有使用其他JS工具去處理HTML(比如說程式碼標註工具),那麼你可能需要注意執行的先後順序;或是採用適當的CSS Selector去跳過不需要處理的區塊。

【遊戲】線上遊戲的轉蛋機制設計與倫理議題

轉蛋機制是免費制線上遊戲(free to play,以下簡稱F2P)中常見的遊戲機制。一言以敝之,這樣的機制消耗了一定數量的遊戲貨幣,並以機率的形式釋放遊戲獎勵。這些遊戲獎勵可能是價值不菲的,例如一張珍貴的角色卡片、關鍵的轉職材料或是罕見的裝備;但更多的是一些不怎麼樣的物品,例如一些普通的角色、雜物或貨幣。

在筆者的工作與遊戲經驗裡,對於F2P線上遊戲而言,轉蛋無疑是最有效能產生營收(monetization)的遊戲機制。儘管我們無法得到任何一款公司提供的確切產品統計,筆者也無法透露自己參與過的專案數據;我們仍然可以透過觀看玩家的社群討論或已公開的抽卡影片與直播,來得到這樣的結論。我想,對於任何遊戲從業人員而言,我想也沒有人會訝異:有不少玩家願意花三五十萬台幣在一款遊戲的轉蛋機上。

在這篇文章中,筆者將討論一些簡單的轉蛋設計方法,以及其所衍發相關的商業倫理議題

簡單純機率

在遊戲製作的實務裡,轉蛋機制往往遠較想像中複雜。從外觀上來看,轉蛋機通常有著一個符合遊戲背景故事的外表(可能是一台轉蛋機、一個召喚怪物的魔法陣、或是打開一個神秘的寶箱)。不論這個機制被包裝得如何,它們通常讓玩家聯想到生活中常見的籤筒抽籤:一個桶子裡有各種的籤,少數代表中獎,但大部分都代表銘謝惠顧或一些雜物。在抽籤的過程裡,每一次抽籤都是獨立事件

轉蛋示意圖 圖片:〈怪物彈珠〉抽取卡片的畫面。轉蛋機制會被包裝成符合遊戲故事背景的樣子。

這樣單純的機制筆者稱之為簡單純機率,以下的機率表即可作為示範。它非常地直觀且好懂:每次抽取都是獨立且公平的;玩家每一抽有5%的機率抽中S卡;若每一抽的價格是100台幣,則抽中一張S卡的平均價格(期望值)會是2000台幣。

簡單純機率表

這樣的簡單純機率看似正常,但其實大有問題。舉例來說,這意味著玩家付出4000台幣(也就是兩倍於期望值的價格),卻仍抽不到S卡的機率其實是13%——如果這發生在遊戲初期,這代表著你有13%具有高度價值的玩家,將承受幾乎毀滅性的遊戲體驗:不只花了大錢卻空手而歸,還必須眼睜睜看著為數眾多的免費試玩玩家在第一抽就抽中S卡(這樣的機率是5%,且第一抽常常是免費的)。

以統計學來說,這是一個典型的二項分布(Binomial Distribution)。下圖顯示了一個p=5%n=40的圖例,當n=40p(x=0)=12.9%,亦即玩家即便願意花4000元,仍然有很高的機率抽不到任何一張S卡(但是有9%的人可以抽到四張)。

二項分布

也許你會這樣想:呃⋯⋯至少這很公平,而且這不過是個遊戲而已——直到你的客服信箱被憤怒的玩家轟炸;臉書專頁與應用程式頁面都被心碎的付費玩家塞滿一星評價與崩潰留言為止。玩家出於對遊戲的愛而付費,當他們承受了毀滅性的遊戲體驗,這樣的愛很容易會轉成熾烈的怨恨。

此外,在台灣與中國,許多遊戲公司往往與最願意花錢的大戶保持私下的、程度不一聯繫,並提供諸如到府收款、專案客服等特殊服務;對於一般在社群網站上活躍的、具有影響力的、願意花個幾千元的玩家,亦有一定程度的交流。當這樣令人不滿的遊戲情境發生時,遊戲公司往往會為了產品形象著想,而願意提供私下的補償。然而,補償的標準要如何設定?寶物的發放要如何控管?又如何避免玩家口耳相傳變成會吵的孩子有糖吃?這對於遊戲經營團隊都會是嚴重的困擾。

更重要的是,有些玩家心碎了,往往也就無聲地離開了。

簡單純機率有什麼問題?

在討論簡單純機率所衍生的的問題前,我們必須先討論何謂遊戲的強度/獎勵。為了方便閱讀過程中的想像,讀者可以將這篇文章提到的「遊戲」設定為幾款熱門的F2P作品,諸如〈神魔之塔〉、〈刀塔傳奇〉或〈皇室戰爭〉。

在一款F2P的的遊戲產品裡,遊戲設計師往往為玩家配置了各種獎勵——又或著說是所謂的強度,來鼓勵玩家的各種遊戲行為。這些行為包含了:

活躍強度/獎勵

意指玩家透過持續地玩遊戲,所能得到的強度與獎勵。你可以把它想像成遊戲時間強度/獎勵的函數。在大部分的F2P遊戲裡,這樣的函數常常是(也必須是)遞減的,但其遞減幅度、以及何時開始遞減,端看產品鎖定的族群而定。舉例來說,學生族群可能可以投注較長的總時數在遊戲裡(放學後有較長的空閒);但對於遊戲的頻率,卻往往不如上班族(上學不能常常用手機)。當產品鎖定的族群不同,活躍強度的函數設計亦會不同。

當時間函數遞減太快,玩家會容易感到無聊(想玩遊戲卻無法得到獎勵);當時間強度遞減太慢時,玩家則會感到強烈的作業感(獎勵捨不得不拿,但又沒那麼多時間一直玩)。

付費強度/獎勵

意指玩家透過付費,所能得到的強度與獎勵,也就是金錢強度/獎勵的函數。舉個極端的例子而言,〈英雄聯盟〉作為一款純電子競技類型的遊戲,其付費幾乎是完全與強度脫鉤的[1],也就是幾乎不存在pay to win的現象;而相反的,〈皇室戰爭〉則透過卡牌的搜集與升級機制,將強度與付費做了一定程度的掛鉤。

社交強度(獎勵)

意指玩家透過與其他玩家互動,所能得到的強度與獎勵。標準的例子例如農場遊戲常見的拜訪獎勵;〈Candy Crush〉系列贈送愛心的機制以及〈神魔之塔〉的公會任務。在長線的遊戲經營裡,社交機制往往是最後能否留下玩家、延長產品壽命的關鍵。

技術強度(獎勵):

意指玩家透過本身的遊戲技術,所能得到的強度與獎勵。這個概念也不難理解:〈英雄聯盟〉就是一個幾乎100%技術強度的遊戲,付費者並不會因為付費而變得更強;而〈皇室戰爭〉大概是付費與技術各佔50%;至於〈刀塔傳奇〉中的競技場裡,付費玩家幾乎可以獲得絕對的優勢。

掌握不同行為獎勵之間的均衡,是遊戲設計至關重要的議題。對於F2P遊戲而言,付費強度的設計又可以說是重中之重。

然而,簡單純機率以及其所連帶的期望值計算,卻無法負擔這樣精細的操控。舉例來說,當一個設計師期待一張S卡所帶來的強度約莫等值於2500~3500台幣之間時,簡單純機率的轉蛋機卻從來無法保證這樣精準的控制。因為,玩家可能會抽到很多張,也可能一張都抽不到。

總得有個辦法吧⋯⋯

儘管簡單純機率有這麼多的問題,轉蛋機制與機率仍然是F2P產品最有效的盈利機制之一。諷刺地是,如果我們直接出售一張S級卡片,並將之標價為三千台幣,玩家很有可能直接嘲笑製作團隊想錢想瘋了:這不過是個遊戲道具,以及一條卑微的資料庫記錄而已。人應該回到現實、多吃大餐、多看電影、多去旅行、買買喜歡的衣服、甚至是拿去吃O喝X,而不該沈溺在虛擬的世界裡⋯⋯

然而,當我們把一張卡片放進轉蛋機裡,並且將期望值設定為三千台幣時,這一切又瞬間變得饒富趣味且值得討論了起來。或許我們可以說:觀察、議論進而承擔風險,並取得風險帶來的期待與獎勵,本身就是遊戲最有趣(甚至是所謂令人成癮)的一部分。

對於轉蛋,必須要有比簡單純機率更好的設計才行。

事實上,你可以放入任何演算法在轉蛋機裡⋯⋯

是的,轉蛋機的背後不過是幾行程式碼[2]。任何演算法,只要能產生貌似隨機的結果,且具備不易被逆推的特色,都可以用來驅動轉蛋機。你討厭遊戲ID是5結尾的玩家,或是email裡有william一詞的人嗎?開間遊戲公司,做出一款好遊戲,你可以請數值企劃和工程師讓這些玩家抽卡抽到懷疑人生[3]⋯⋯

接下來筆者會舉出些常見但基本的轉蛋設計技巧,以及其所可能對應到的遊戲體驗,作為範例。

範例1:取出不放回

取出不放回是一個基礎統計常討論的模型。當應用在遊戲設計裡,它可以確保玩家至少在n抽裡獲得一個或數個以上的大獎,卻仍然保持了機率所帶來的有趣性。

以下圖為例,這台簡化的轉蛋機以20為一個循環,每次20抽裡會確保玩家得到一張S卡。相較於簡單純機率,超級倒霉與超級好運的極端狀況就消失了,對於付費與強度的關聯控制也變得更加精準,但仍然保有一些機率的樂趣。

取出不放回

當然,這樣的設計也並非全無缺點,例如某些具備經驗的玩家可以精準地抓出其中的規律,並公告在網路社群上,這可能使得玩家對於轉蛋失去期待感。不過,當母池容量變得巨大,且混合了更多隨機機制時,其實出獎規律仍然不是那麼容易抓到的。

在實務裡,這樣的機率配置可能是隱匿的。但設計師亦可採用部分揭露或是完全揭露的機制:前者會讓玩家意識到自己中特獎的機率逐漸提高[4];而後者則會直接在遊戲的使用者介面上,讓玩家理解到母池的概念。

範例二:條件對應

這如之前所提:在過分簡單的外表下,轉蛋機制的背後可以是任何型式的演算法。對於遊戲工程師而言,只要數值企劃能提供明確的演算過程,將之透過程式實踐通常不會是太困難的事。

條件對應亦是一個可使用的設計手法。其原理在於:在實踐機率的運算之前,先以其他指摽進行非機率的邏輯切換。

舉個簡單的例子,在下圖中,轉蛋機制會記錄玩家的總抽取次數,再以抽取次數進行除以三的餘數運算(%3),再依照餘數對應至不同的簡單純機率表格。在這個例子裡,玩家會不斷地依序抽取到卡牌、武器、貨幣。

次數條件對應

當然,這樣過分簡化的機制是很容易被玩家察覺的。然而,遊戲設計師可以將條件對應機制與其他的模式結合,例如前段所提到的取出不放回

下圖中我們示範了一個混合了條件對應+取出不放回+簡單純機率的轉蛋設計。在一個有十顆彩球的母池裡;每種顏色的彩球對應到了不同等級的獎勵。在這樣的抽獎循環裡,玩家每十次抽獎都會得到一張S卡/兩張普通卡/兩件裝備/五個遊戲貨幣包。至於玩家會拿到哪一張S卡,哪兩張普通卡呢,以及以什麼順序抽到各種獎勵呢——這部分或許就是設計師願意讓機率決定並作為樂趣的範圍了。

綜合應用

範例三:付費與強度之外的考量

轉蛋機制的機率設計往往也不只是為了坑錢這麼單純的考量。舉例來說,在卡牌遊戲裡,玩家手牌的組合往往是前期體驗展開社交與合作的重要關鍵——作為一個設計師,可能不會希望玩家在前期裡滿手都是坦克而沒有攻擊手;又或是過早取得了完全均衡的團隊,以至於失去和其他玩家互動、租借卡牌的需求。

因此,牌組的多元性也可以是設計師操作機率的考量之一。對於數值企劃而言,這其實是一種透過數學與統計創造體驗的技術。舉例來說,當設計師規劃了數組可能的策略牌組時,他可以要求工程師以條件對應的方式,將玩家依照用戶ID的尾數(玩家甚至可能不知道這個數字的存在),將轉蛋機對應至不同的抽卡路徑。這樣的做法,可以確保玩家獲得某種(鬆散但不是絕對的)牌組雛形,也能確保多元的遊戲話題與玩家之間的合作。

轉蛋機制的倫理爭議與規範的困難

行文至此,讀者或許已經可發現其爭議之處——轉蛋機制以一個過分簡單的外表、絕少的資訊揭露,來掩蓋了十分複雜的機率黑盒子。當它被用作主要的營收機制時,其實是可能存在類似賭博與詐欺的疑慮的。舉個例子來說:當玩家看到一個特殊的轉蛋活動,宣告「S卡機率加倍時」,其背後的機率設定「可以」是完全沒變的。除此之外,這樣的機率也可以透過程式,進行動態調整

這的確是具有爭議的。在玩家的社群討論板裡,我們也常常看到因為消費聽驗不佳的用戶們,在討論著如何成功地與Google與Apple的客服申請退款。

這很有可能早已造成了兩大平台的困擾。儘管它們都曾試圖採取規範行動——主要著重在公開內容一項,但截至2019年中,這樣的規範似乎尚未產生具體的影響力。其關鍵在於,不論是公開內容甚至是機率,都仍然侷限於簡單純機率的想像裡,對於更多複雜的設計方法,諸如取出不放回條件對應以及各種機率設計的交互應用,還是無以規範。更重要的是:兩大平台既不可能要求廠商公開轉蛋機制的原始碼[5];亦無以透過外在的實驗去進行逆向檢核,以至於即便廠商公布了資訊,玩家也無從得知其是否為真。

公布機率 圖片:〈皇室戰爭〉中揭露寶箱內容的介面。筆者認為,複雜的機率很難用介面顯示;顯示了也不一定包含完整的細節;完整了也不一定是真的;就算是真的,還是可以用程式進行動態調整。

總結地說,遊戲設計的複雜性更加深了規範的困難。首先,正如文章中所言:轉蛋機制擁有多元的外皮,要執行有效的規範,更不容易。其次;部分的轉蛋亦包含了付費以外,諸如遊戲體驗與平衡類的考量;最後,在當代的遊戲設計裡,轉蛋使用的貨幣亦不全然來自於付費——以經典F2P遊戲〈神魔之塔〉為例,其抽卡用的貨幣可能是免費發送(諸如活躍獎勵與過關獎勵),亦可能是來自於付費購買的。如果以消費透明為由,要求廠商通盤公布可能屬於商業知識的機率配置,亦未必公允。

最後的最後,當遊戲營運商透過Google、Steam、Apple進行全球發行已成常態,區域政府的法規是否能有效規範並公平地規範每一個產品,亦是一大疑慮。舉例來說,如果台灣自行通過了較嚴格消費者保護法律,執法者卻只能針對本地的遊戲研發者執法,卻無力規範主流的跨國遊戲。這無疑讓競爭變得更不公平。

2019年的美國立法嘗試:兒童遊戲成癮防制法

2019年上半年,美國參議員Josh Hawley提出了名為The Protecting Children from Abusive Games Act的法案草案(暫譯:兒童遊戲成癮保護法),亦引起了遊戲開發者一定程度的重視。參議員Hawley主張:「以兒童為目標客群的遊戲,不該被允許從成癮行為裡盈利。當兒童玩著以成人為目標客群設計的遊戲時,他們亦應該被隔絕於成癮性的小額付費體驗之外。那些刻意從兒童身上剝削營收的遊戲開發者,必須面臨法律的懲處」

在具體的案例上,他標示出了兩大最被濫用的設計模式,其一是Pay to win[6];其二是Loot Box(也就是我們所謂的轉蛋機制)。在申論中,參議員Hawley主張:「Loot Box機制廣泛被採用於免費與付費的遊戲裡。這樣的機制提供了隨機的付費獎品,並結合了pay to win令人黏著的特性;以及沿襲自賭博的成癮行為」[7]

這是一個很有趣的提案,也是F2P的商業模式再次承受了來自法律規範的挑戰。礙於篇幅與精力,本篇文章在此點到為止,有興趣的讀者可自行追蹤相關報導與後續。

倫理議題

更廣遠地看,這世界上有著販賣有害健康食物的餐廳、政府做莊家的彩券與運動彩券、特許的民營賭場、配備高級音響但安全設備不足的汽車、文案曖昧但幾乎沒有實質效用的減肥藥與保健食品⋯⋯遊戲轉蛋究竟應該被做到何種程度的規範?我想並不容易回答。而遊戲作為常常被主流價值污名化與缺乏瞭解的標的,當又再次變成法律規範的焦點時,亦不難想像開發者心中是會有所不平的(你看過彩卷或刮刮樂公告中獎率嗎?什麼該被歸類為所謂「面向兒童」設計的遊戲?薯條和冰淇淋算是面向兒童設計的垃圾食物嗎?那迪士尼樂園裡到處陳列的超可愛Shellie May熊熊呢?)。

這題並不好回答,筆者也沒有打算在本篇文章中回答[8]。可以確定的是,作為一個開發者,亦不該一味迷信夠坑=賺錢=成功的教條。正如文章中所言,數值企劃是一門透過數學與統計創造體驗的技術,在過去的五年十年內,更好、副作用更低、更有回饋感與創意的盈利模式持續被優秀的開發者創造出來。團隊除了適時地討論、確認自己內部的倫理自律標準外,亦應持續重視、精進設計的技術,才能做出好的作品。


註解

[1] 英雄聯盟的強度仍然與付費有著些許微妙的掛鉤。例如付費玩家可以擁有更深的角色池以及更多的天賦頁面,可以針對對手做出更好的反制配置。

[2] 嚴格來說,應該是好幾十行百行和一堆資料庫配置。

[3] 但通常是你自己在遊戲研發的過程中會比較懷疑人生⋯⋯

[4] 例如,提供一個諸如「運氣指數」的指標,然後隨著母池的清空,逐漸顯示為普通/略高/極高。當玩家知道這些機率是由自己過去的消費所堆疊起來時,也常常更難以放棄。

[5] 就算真的公開原始碼,也不能確保伺服器上運作的版本與數值是公開的版本。

[6] 熟悉遊戲產業發展歷史的玩家與開發者應該都可以想像,完全禁止pay to win,也就是所謂付費與強度的連結,是一件多麽荒謬的事。

[7] 本段引言取材自網路。美國參議院網站,(May 8, 2019), Senator Hawley to Introduce Legislation Banning Manipulative Video Game Features Aimed at Children. 擷取自 https://www.hawley.senate.gov/senator-hawley-introduce-legislation-banning-manipulative-video-game-features-aimed-children

[8] 事實上是,社會裡的立法與規範形成的過程,往往也不只是透過倫理與應然的辯論而產生的。對於實力尚未充足的開發者而言,花太多時間討論倫理不如靜觀其變,打好研發技術才是正途。

【程式】我需要學Python資料科學嗎?

近年來自學coding蔚為風潮,很常被朋友問到:「我需要學Python資料科學嗎?」。具體地說,被提出的課程通常包含了資料處理、視覺化,有的還包含了諸如網路爬蟲或是API串接[1]。

詢問者的動機通常是想充實專業,應用於學術或是工作領域。另一方面,也因為鋪天蓋地的媒體新聞與課程廣告,感到焦慮。

先講結論:個人認為,對於非資訊背景的學生,速成課程的學習效果相當有限;而大部分來詢問的人,未必真的需要;最後,儘管Python是個很棒的工具,但「會寫程式/Python」這件事,被包裝成了一個太過美好的想像。

而我的習慣是這樣的:如果一個大哉問被問了許多次;而又算是我有經驗的領域;我就會把它寫成文章。 所以我們就開始吧。

Python資料科學是什麼?

首先,請見附圖[2]。在這個結構裡,資料科學——或是一場有意義的數據專案(諸如行銷、選舉、乃至最近很常見的肺炎數據呈現),個人認為需要三個領域的完備:

  • 專項領域知識
  • 數學與統計學
  • 電腦應用 <= Python在這裡

Python Data Science

請特別注意前兩項的存在!首先,Python有許多套件,可以協助我們處理數據,實作出「專項領域知識」+「數學與統計學」的交集——然而在這個場景裡,Python本身並無法自外於兩者而產生意義。其次,Python的操作並不屬於太高深的計算機科學[3],你也不會因為只熟悉Python(或任何一種程式語言)而獲得一位(認真學習多年的)資訊本科生的同等能力;或是可以因此去Google開發無人車。比較好的比喻是:你可以把Python想像成威力更強大但也更不容易操作的Excel:它們都有很棒的資料處理能力,但要從資料中提取出價值,還是要倚賴領域本身的理論基礎才行。

再舉個幾個例子吧:電鍋很好用,但電鍋不會讓你成為大廚;一組完備的工具機,也不會讓你成為一個優秀的水電工;Python可以幫你處理股票資料,但不保證你能做出有意義的分析甚至盈利——而用Excel就做得風生水起的投資戶大有人在。

這段用詞比較負面,但我想透過這樣的措辭,來平衡那些課程廣告中過分美好的(他們也知道不是事實但仍然留下的)暗示。

所以⋯⋯我需要學Python資料科學嗎?

來詢問我的大多是商管學院出身的工作者。坦白說,我覺得最泛用的答案是「NO」,以及接著的「unless……」

僅管Python很棒,但根據個人經驗,對於非資訊本科生而言,一場成功的入門學習通常必須滿足以下(任一)條件:

  • 你是一個專業人士,你已經相當熟悉Excel或其他試算表軟體,且已經遭遇到Excel無法滿足的使用情境。
  • 你是一個專業人士,你現在的工作團隊已經在使用Python(或其他資料庫語言)。在有實務用例與演練的情況下,學習效率會有十倍百倍的提升。
  • 你是一個高中生或大學生,你擁有充足的學習時間,也正在處於廣泛探索知識的階段。在這個情境下,程式語言會是一個很棒的通識主題——同樣的道理我也會推薦你學習經濟學、社會學、數學、法律⋯⋯而一但開始學programming,別忘了務必多寫code以及實作對你有意義的專案,唯有如此才能真正內化這門技藝。
  • 你是一個研究生,你想要在研究領域裡實作量化資料分析,並將過程與方法有系統地與其他研究者分享。
  • 如果以上皆非;但你有充足的時間與意願學習這項技術;也有想要實作的應用(例如股票分析)。
  • 再次以上皆非;但你想要在履歷表放上這項專業;且你是個善於包裝的人;同時你相信你現在或未來的雇主崇拜/敬畏會寫code的人(即便沒有實務上的價值依然如此?)

Why?

對於學習Python說NO,這樣的建議其實是相當違反潮流的。我想原因如下:

  • 我沒有要賣你課程(超級100%正解)。
  • 儘管Python已經是相當高階的(面向人類而非機器的)程式語言,但對於一般沒有資訊背景的人而言,仍然是易學卻難通。在大部分強調速成的網路課程裡,為了提升學生的購買意願以及成就感,往往快速進入資料處理與視覺化的部分,而略過了基本的安裝、除錯、command line操作、版本控制、程式語言的基本概念;以及Python的資料結構、基本語法,與套件管理。這樣的課程雖然速成,但學生並不會獲得解決問題、有效率地寫code,以及舉一反三的能力。常見的結局是:學生只能拿著教材提供的代碼修修補補,但不具備真正實作專案的能力。如果又沒有實務上的用例,這樣零碎的知識很快就被會遺忘。
  • 如果你是一個已累積一定專業的工作者,我假定你的時間是寶貴的,且你的投入是期待回饋的。因此,除非已有明確的需求,散亂地學習的投資報酬率並不高。另外,不深入地學習也無法讓Python或資料分析成為你的第二專業——那些速成課程的內容對於資訊本科生而言,往往只是翻翻文件就(應該要)做得出來的東西。但無可取代的永遠是你的專項領域知識,以及對於數學及統計學的掌握。而這都是一般Python課程無法給你的。
  • 承上,Excel有什麼不足?如果你已經親身遭遇並能回答出來,那我相信你會獲得遠超過平均的學習成果。
  • 更不用說「Python+AI+大數據+爬蟲」的大雜燴課程了。這些課程就像健身房會籍一樣,絕大部分的人付費後並沒有妥善使用,而只是被販售恐懼以及購買美好的想像而已。並不是說課程本身有什麼過不去的惡點,但不論是投入任何專業領域,請問問自己:你真的準備好放下其他事、充足的補給、十足的熱情,以及不怕卡關的韌性了嗎?
  • 當然,如果你覺得自己只是想探索,也不介意花一點時間,那就出發吧。

怎麼學?

這個段落偏向主觀,有很多有經驗者見解也和我不同。如果你心意已決,請斟酌自已的個性與過去的學習經驗,作為參考即可。

首先,在「時間寶貴」與「實作優先」的兩項前提下,我推薦線上課程勝於實體課程。另外,個人是獨處與根性的信徒,如果你總是相信「有實體的活動或同學比較不會怠惰」,那我(偏狹地)猜測你可能不適合寫程式(笑)。

其次,如果語言與經濟能力許可,我建議選購英文的付費課程。長遠來說,習慣閱讀英文文件以及論壇,能達到最好的學習效果;另外,付費課程往往有更好的架構,可以避免初學者無法有效過濾學習素材的問題。個人用過的線上服務包含teamtreehouse.com與datacamp,都稱不上完美,但也絕對不至於帶來關鍵性的失敗。

另外,許多課程會提供網路介面與沙盒讓你寫code。儘管這都是不錯的初學工具,但你應該要很快地離開這些介面,快速掌握自己本機上的開發環境才行——如果你要寫實戰的code,總不可能寫在教學網站的編輯器裡對吧?

最後,別忘了Python是一個泛功能(general purpose)的程式語言。資料分析套件只是它眾多功能其中的一項而已。儘管是一個很棒的入門點,但個人建議你必須先懂Python,然後才能好好操作這些套件並讀懂文件——這包含了Python的基本語法、資料結構、函式應用、物件導向編程、套件管理等。另外,你可能也需要花時間熟悉command line以及版本控制等進階的電腦應用。

結語

對於非資訊背景者而言,想要有效學習Python資料分析,最好已經具備具體的需求,以及投入充足的時間(個人體感估計是:平均每週十小時,持續半年),才能累積出明顯的成效——別忘了,這意味著排擠其他生活中的事務,包含已有的專業、人際關係、休息與娛樂等⋯⋯

總而言之。寫程式對某種類型的人類會是很快樂的事。商人自有話術,商品未必邪惡,但coding絕非每個人都必須具備的技術。萬事皆有成本;而玩票近似於不玩——剩下的,就請自行斟酌了。


註解

[1] 嚴謹地說,這樣的課程其實是以應用為主的。稱之為科學太沈重了。

[2] 改編自:Jake VanderPlas著|2017|Python Data Science Handbook|第一章|O’REILLY發行

[3] 有一派主張「計算機工程」更精準於「計算機科學」。本文不討論此議題且並列兩者。