Clojure로 짜본 오목 콘솔 프로그램

easbui·2023년 7월 5일
0

클로저를 입문하고 토이 사이즈 프로그램을 짜보면서 감을 익혀야겠다 싶어, 콘솔 입출력을 통한 오목 프로그램을 짰다. 만약 절차적인 프로그래밍 패턴을 이용했다면, 바둑판을 객체나 혹은 배열 등으로 모델링하여 바둑판의 상태를 변화시키면서 프로그램이 진행되었을 것이다. 그러나 함수형 프로그래밍 패턴을 이용하면서 현재 바둑판의 모습과 놓을 수를 입력받아 다음 바둑판의 상태를 출력하는 함수를 짜고, 이를 재귀적으로 호출하면서 프로그램을 진행시켰다.

SF 작가 테드 창이 쓴 '나와 당신의 이야기'를 원작으로 한 드니 빌뇌브 감독의 영화 <컨택트>를 인상깊게 본 적있었다. 함수형 프로그래밍에 도전해보면서, 그 영화와 흡사한 점이 많다는 생각이 들었다.

! 영화 <컨택트> 스포일러 주의 !

영화에서 등장하는 외계인 (헵타포드)들은 원형의 언어를 사용한다. 인간의 언어가 쓰고 읽음에 있어 선형적인 특징을 가지며, 시제와 같은 특징으로 인해 특정한 맥락의 상황을 기술하는데 초점이 맞춰진 반면, 외계인의 원형의 언어는 무시제적이며 다양한 맥락의 상황을 하나의 원형에 표현하므로 여러 맥락으로 해석된다. 이 원형의 언어를 해석하기 위해 고군분투하는 주인공은 결국 과거와 미래로부터 구분된 현재의 삶을 보는 것에서 이들을 총체적으로 인식하게 된다. (그래서 과거의 아픔처럼 연출되었던 장면들이 사실 다가올 미래의 운명과도 같은 것임이 드러난다)

절차적 프로그래밍과 함수형 프로그래밍에서 상태를 바라보는 차이가 이와 비슷하게 보인다. 절차적 프로그래밍에서 하나의 상태에서 다른 상태로 '변화'하는 것이지만, 함수형 프로그래밍에선 하나의 상태에서 다른 상태가 계산되어지는 것 뿐이다. 단지 평가되지 않았을 뿐, 모든 상태는 이미 존재하고 있는 것이다.

결과적으로 절차적 프로그래밍에선 현재의 상태에서 다음 상태로 변화시키기 위해선 무엇을 바꿔야 할지에 대해 생각하게 되는 것에 반해, 함수형 프로그래밍은 지금의 상태와 다음의 상태의 수학적 관계에 대해서 생각하게 된다. 어떤 나무들이 모여 숲을 이루는지를 보는 것이 전자라면, 나무들이 어떻게 모여 숲을 이루는지를 보는 것이 후자인 셈이다. (정확한 비유는 아니겠지만...)

영화에서는 언어가 인간의 사고에 미치는 영향에 관한 가설인 사피어-워프 가설을 핵심적인 소재로 삼고 있다. 이 가설이 옳은 지에 대해선 갑론을박이 있으나, 결국 프로그래머에게 있어선 그 언어를 사용하기 위해서라도 그 언어가 지향하는 바에 따라 사고하도록 노력해하는 것은 바뀌지 않는다. 이를 종종 되새기며 공부해야겠다.


소스코드

(ns omok.core)
(require 'clojure.set 'clojure.string)

(def BOUNDARY 10) ;; 10 BY 10 바둑판

(defn line-complete? [[r c] checked] ;; 한 줄(5개)이 완성되었는지 체크
  (some identity (for [dir [:up :up-right :right :down-right]]
                   (loop [offset -4
                          cnt 0]
                     (if (<= -4 offset 4)
                       (if (< cnt 5)
                         (if (contains? checked (cond
                                                  (= dir :up) (conj `() c (+ r offset))
                                                  (= dir :up-right) (conj `() (+ c offset) (- r offset))
                                                  (= dir :right) (conj `() (+ c offset) r)
                                                  (= dir :down-right) (conj `() (+ c offset) (+ r offset))
                                                  :else nil))
                           (recur (inc offset) (inc cnt))
                           (recur (inc offset) 0))
                         true)
                       false)))))

(defn parse-int [number-string]
  (try (Integer/parseInt number-string)
    (catch Exception e nil)))

(defn read-pos [checked boundary] ;; 사용자로부터 다음 수 위치 입력
  (let [in (read-line)]
    (if (re-matches #"^\d+:\d+$" in)
     (let [[r c] (map parse-int (clojure.string/split in #":"))]
       (if (and (<= 0 r (dec boundary)) (<= 0 c (dec boundary)))
         (if (contains? checked (conj `() c r))
           (do (println "Already checked, Check Again") (recur checked boundary))
           (conj `() c r))
         (do (println "Out of Boundary, Check Again") (recur checked boundary))))
     (recur checked boundary))))


(defn print-board [black-checked white-checked boundary] ;; 바둑판 출력
  (println "[BOARD]")
  (println (apply str "#" (range boundary)))
  (doseq [r (range boundary)]
    (println (str (apply str r (map (fn [c] (cond (contains? black-checked (conj `() c r)) "□"
                                                  (contains? white-checked (conj `() c r)) "■"
                                                  :else "*")) (range boundary)))))))

(defn next-turn [checked-b checked-w player boundary] 
  (print-board checked-b checked-w boundary) ;; 먼저 바둑판 출력
  
  (let [pos (read-pos  (conj (clojure.set/union checked-b checked-w)) boundary)
        checked-b-next  (if (= player :black) (conj checked-b pos) checked-b) ;; 수를 두고 난 다음 바둑판 (흑)
        checked-w-next  (if (= player :white) (conj checked-w pos) checked-w) ;; (백)
        done (line-complete? pos (if (= player :black) checked-b-next checked-w-next))] ;; 한 줄이 완성되어 끝난 것인지 체크
    
    (if (not done)
     (do
       (doseq [i (range 5)] (println ""))
       (println (str "Player " (if (= player :black) "black" "white") ", it's your turn.")) ;; 다음 차례 안내
       (recur checked-b-next checked-w-next (if (= player :black) :white :black) boundary))
     (do
       (doseq [i (range 5)] (println ""))
       (println (str "****** Player " (if (= player :black) "black" "white") " is winner!"))
       (println "== RESULT ==")
       (print-board checked-b-next checked-w-next boundary)))))


(defn main []
  (println "START GAME")
  (next-turn #{} #{} :black BOUNDARY))
profile
개발자 - 프로그램을 개발새발짜는 사람

0개의 댓글