import java.io.*;
import java.util.*;
import java.awt.geom.*;
import java.math.*;

/**
 * Try considering all possible orders to traverse the lines in to form
 * a polygon. Make sure that no pair of adjacent lines are parallel,
 * and check that the polygon does not intersect itself. 
 * This solution uses BigInteger and a Fraction representation to maintain
 * precision throughout. 
 *
 * @author Finn Lidbetter
 */

public class findingpolly_finn_bigint {
  public static void main(String[] args) throws IOException {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

    int n = Integer.parseInt(br.readLine());
    FracLine[] lines = new FracLine[n];
    for (int i=0; i<n; i++) {
      String[] s = br.readLine().split(" ");
      int x1 = Integer.parseInt(s[0]);
      int y1 = Integer.parseInt(s[1]);
      int x2 = Integer.parseInt(s[2]);
      int y2 = Integer.parseInt(s[3]);
      lines[i] = new FracLine(x1,y1,x2,y2);
    }
    FracPt[][] intersections = new FracPt[n][n];
    for (int i=0; i<n; i++) {
      for (int j=0; j<n; j++) {
        if (i==j)
          continue;
        intersections[i][j] = FracLine.intersection(lines[i], lines[j]);
      }
    }
    // Try all permutations of the lines.
    int[] order = new int[n-1];
    int count = 0;
    for (int i=0; i<n-1; i++) {
      order[i] = i;
    }
    while (order!=null) {
      int[] wholeOrder = new int[n];
      for (int i=0; i<n-1; i++) {
        wholeOrder[i] = order[i];
      }
      // Fix the last line at the end to avoid double counting rotations.
      wholeOrder[n-1] = n-1;
      if (isSimple(wholeOrder, intersections)) {
        count++;
      }
      order = nextPermutation(order);
    } 
    // We double counted reversals.
    System.out.println(count/2);
  }

  static int[] nextPermutation(int[] c) {
    int first = getFirst(c); 
    if (first == -1) 
      return null;
    int toSwap = c.length - 1;
    while (c[first] >= c[toSwap]) 
      --toSwap;
    swap(c, first++, toSwap); 
    toSwap = c.length - 1;
    while (first < toSwap) 
      swap(c, first++, toSwap--);
    return c; 
  }

  static int getFirst(int[] c) {
    for (int i = c.length - 2; i >= 0; --i) 
      if (c[i] < c[i + 1]) 
        return i;
    return -1; 
  }

  static void swap(int[] c, int i, int j) { 
    int tmp = c[i]; 
    c[i] = c[j]; 
    c[j] = tmp; 
  }

  static boolean isSimple(int[] order, FracPt[][] intersections) {
    // Return true iff the given order of lines defines a simple polygon (no self intersections).
    int n = order.length;
    FracPt[] pts = new FracPt[n];
    FracSegment[] fracSegments = new FracSegment[n];
    Line2D[] segments = new Line2D.Double[n];
    for (int i=0; i<n; i++) {
      int j = order[i];
      int k = order[(i+1)%n];
      if (intersections[j][k]==null)
        return false;
      pts[i] = intersections[j][k];
    }
    for (int i=0; i<n; i++) {
      for (int j=i+1; j<n; j++) {
        if (pts[i].equals(pts[j]))
          return false;
      }
    }
    for (int i=0; i<n; i++) {
      fracSegments[i] = new FracSegment(pts[i], pts[(i+1)%n]);
      
      segments[i] = new Line2D.Double(
        pts[i].x.n.doubleValue()/pts[i].x.d.doubleValue(), 
        pts[i].y.n.doubleValue()/pts[i].y.d.doubleValue(), 
        pts[(i+1)%n].x.n.doubleValue()/pts[(i+1)%n].x.d.doubleValue(),
        pts[(i+1)%n].y.n.doubleValue()/pts[(i+1)%n].y.d.doubleValue()
      );
      
    }
    for (int i=0; i<n; i++) {
      for (int j=i+2; j<n; j++) {
        if ((j+1)%n==i)
          continue;
        if (FracSegment.intersects(fracSegments[i], fracSegments[j])) {
          return false;
        }
      }
    }
    return true;
  }
}
class Frac {
  static Frac ZERO = new Frac(0, 1);
  static Frac ONE = new Frac(1, 1);
  BigInteger n,d;
  public Frac(long nn, long dd) {
    this(new BigInteger(""+nn), new BigInteger(""+dd));
  }
  public Frac(BigInteger nn, BigInteger dd) {
    if (nn.equals(BigInteger.ZERO)) {
      n = BigInteger.ZERO;
      d = BigInteger.ONE;
    } else {
      BigInteger gcf = gcd(nn.abs(), dd.abs());
      n = nn.divide(gcf);
      d = dd.divide(gcf);
      if (d.compareTo(BigInteger.ZERO)<0) {
        n = BigInteger.ZERO.subtract(n);
        d = BigInteger.ZERO.subtract(d);
      }
    }
  }
  public Frac add(Frac f2) {
    return new Frac(n.multiply(f2.d).add(d.multiply(f2.n)), d.multiply(f2.d));
  }
  public Frac sub(Frac f2) {
    return new Frac(n.multiply(f2.d).subtract(d.multiply(f2.n)), d.multiply(f2.d));
  }
  public Frac mult(Frac f2) {
    return new Frac(n.multiply(f2.n), d.multiply(f2.d));
  }
  public Frac divide(Frac f2) {
    return new Frac(n.multiply(f2.d), d.multiply(f2.n));
  }
  static BigInteger gcd(BigInteger a, BigInteger b) {
    return b.equals(BigInteger.ZERO) ? a : gcd(b, a.mod(b)); 
  }
  public boolean equals(Object o) {
    Frac f2 = (Frac) o;
    return n.equals(f2.n) && d.equals(f2.d);
  }
  public String toString() {
    return String.format("%s/%s", n.toString(),d.toString());
  }
}
class FracPt {
  Frac x,y;
  public FracPt(Frac xx, Frac yy) {
    x = xx;
    y = yy;
  }
  public boolean equals(Object o) {
    FracPt f2 = (FracPt)o;
    return x.equals(f2.x) && y.equals(f2.y);
  }
  public String toString() {
    return String.format("(%s,%s)", x.toString(), y.toString());
  }
}
class FracSegment {
  FracPt p1, p2;
  public FracSegment(FracPt pp1, FracPt pp2) {
    p1 = pp1;
    p2 = pp2;
  }
  public static boolean intersects(FracSegment s1, FracSegment s2) {
    Frac dx1 = s1.p2.x.sub(s1.p1.x);
    Frac dy1 = s1.p2.y.sub(s1.p1.y);
    Frac dx2 = s2.p1.x.sub(s1.p1.x);
    Frac dy2 = s2.p1.y.sub(s1.p1.y);
    int side1 = side(dx1,dy1,dx2,dy2);
    dx2 = s2.p2.x.sub(s1.p1.x);
    dy2 = s2.p2.y.sub(s1.p1.y);
    int side2 = side(dx1,dy1,dx2,dy2);
    if (side1==side2)
      return false;
    dx1 = s2.p2.x.sub(s2.p1.x);
    dy1 = s2.p2.y.sub(s2.p1.y);
    dx2 = s1.p1.x.sub(s2.p1.x);
    dy2 = s1.p1.y.sub(s2.p1.y);
    int side3 = side(dx1,dy1,dx2,dy2);
    dx2 = s1.p2.x.sub(s2.p1.x);
    dy2 = s1.p2.y.sub(s2.p1.y);
    int side4 = side(dx1,dy1,dx2,dy2);
    if (side3==side4)
      return false;
    return true;
  }
  static int side(Frac dx1, Frac dy1, Frac dx2, Frac dy2) {
    Frac result = dx1.mult(dy2).sub(dy1.mult(dx2));
    return result.n.compareTo(BigInteger.ZERO);
  }
  public String toString() {
    return String.format("[%s, %s]", p1.toString(), p2.toString());
  }
}
class FracLine { // ax+by=c
  Frac a,b,c;
  FracLine(long x1, long y1, long x2, long y2) { 
    a = new Frac(y1 - y2, 1); 
    b = new Frac(x2 - x1, 1); 
    c = new Frac(x2*y1 - x1*y2, 1); 
    normalise(); 
  }
  void normalise() { 
    if (b.n.equals(BigInteger.ZERO)) {
      c = c.divide(a);
      a = Frac.ONE;
      b = Frac.ZERO;
    } else { 
      a = a.divide(b);
      c = c.divide(b);
      b = Frac.ONE; 
    } 
  }
  static FracPt intersection(FracLine l1, FracLine l2) { // Returns null if l1 and l2 are parallel
    if (l1.a.sub(l2.a).n.equals(BigInteger.ZERO) && l1.b.sub(l2.b).n.equals(BigInteger.ZERO))
      return null;
    Frac x = null;
    Frac y = null;
    if (l1.b.n.equals(BigInteger.ZERO)) {
      x = l1.c.divide(l1.a);
      y = (l2.c.sub(l2.a.mult(x))).divide(l2.b);
    }
    else if (l2.b.n.equals(BigInteger.ZERO)) {
      x = l2.c.divide(l2.a);
      y = (l1.c.sub(l1.a.mult(x))).divide(l1.b);
    }
    else if (l1.a.n.equals(BigInteger.ZERO)) { 
      y = l1.c.divide(l1.b);
      x = (l2.c.sub(l2.b.mult(y))).divide(l2.a);
    }
    else if (l2.a.n.equals(BigInteger.ZERO)) { 
      y = l2.c.divide(l2.b);
      x = (l1.c.sub(l1.b.mult(y))).divide(l1.a);
    }
    else { 
      x = (l1.c.sub(l2.c)).divide(l1.a.sub(l2.a));
      y = (l1.c.sub(l1.a.mult(x))).divide(l1.b);
    }
    return new FracPt(x, y);
  } 
}


